Merge pull request #40 from zitadel/oidc

feat: oidc proxy, handle authRequest, callback
This commit is contained in:
Max Peintner
2023-09-29 08:30:04 +02:00
committed by GitHub
41 changed files with 962 additions and 460 deletions

View File

@@ -1,12 +1,12 @@
import { Session } from "@zitadel/server"; import { Session } from "@zitadel/server";
import { listSessions, server } from "#/lib/zitadel"; import { listSessions, server } from "#/lib/zitadel";
import { getAllSessionIds } from "#/utils/cookies"; import { getAllSessionCookieIds } from "#/utils/cookies";
import { UserPlusIcon } from "@heroicons/react/24/outline"; import { UserPlusIcon } from "@heroicons/react/24/outline";
import Link from "next/link"; import Link from "next/link";
import SessionsList from "#/ui/SessionsList"; import SessionsList from "#/ui/SessionsList";
async function loadSessions(): Promise<Session[]> { async function loadSessions(): Promise<Session[]> {
const ids = await getAllSessionIds(); const ids = await getAllSessionCookieIds();
if (ids && ids.length) { if (ids && ids.length) {
const response = await listSessions( const response = await listSessions(
@@ -20,7 +20,13 @@ async function loadSessions(): Promise<Session[]> {
} }
} }
export default async function Page() { export default async function Page({
searchParams,
}: {
searchParams: Record<string | number | symbol, string | undefined>;
}) {
const authRequestId = searchParams?.authRequestId;
let sessions = await loadSessions(); let sessions = await loadSessions();
return ( return (
@@ -29,7 +35,7 @@ export default async function Page() {
<p className="ztdl-p mb-6 block">Use your ZITADEL Account</p> <p className="ztdl-p mb-6 block">Use your ZITADEL Account</p>
<div className="flex flex-col w-full space-y-2"> <div className="flex flex-col w-full space-y-2">
<SessionsList sessions={sessions} /> <SessionsList sessions={sessions} authRequestId={authRequestId} />
<Link href="/loginname"> <Link href="/loginname">
<div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all"> <div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all">
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5"> <div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">

View File

@@ -0,0 +1,122 @@
import {
createCallback,
getAuthRequest,
listSessions,
server,
} from "#/lib/zitadel";
import { SessionCookie, getAllSessions } from "#/utils/cookies";
import { Session, AuthRequest, Prompt } from "@zitadel/server";
import { NextRequest, NextResponse } from "next/server";
async function loadSessions(ids: string[]): Promise<Session[]> {
const response = await listSessions(
server,
ids.filter((id: string | undefined) => !!id)
);
return response?.sessions ?? [];
}
function findSession(
sessions: Session[],
authRequest: AuthRequest
): Session | undefined {
if (authRequest.hintUserId) {
console.log(`find session for hintUserId: ${authRequest.hintUserId}`);
return sessions.find((s) => s.factors?.user?.id === authRequest.hintUserId);
}
if (authRequest.loginHint) {
console.log(`find session for loginHint: ${authRequest.loginHint}`);
return sessions.find(
(s) => s.factors?.user?.loginName === authRequest.loginHint
);
}
return undefined;
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const authRequestId = searchParams.get("authRequest");
if (authRequestId) {
const { authRequest } = await getAuthRequest(server, { authRequestId });
const sessionCookies: SessionCookie[] = await getAllSessions();
const ids = sessionCookies.map((s) => s.id);
let sessions: Session[] = [];
if (ids && ids.length) {
sessions = await loadSessions(ids);
} else {
console.info("No session cookie found.");
sessions = [];
}
// use existing session and hydrate it for oidc
if (authRequest && sessions.length) {
// if some accounts are available for selection and select_account is set
if (
authRequest &&
authRequest.prompt.includes(Prompt.PROMPT_SELECT_ACCOUNT)
) {
const accountsUrl = new URL("/accounts", request.url);
if (authRequest?.id) {
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
}
return NextResponse.redirect(accountsUrl);
} else {
// check for loginHint, userId hint sessions
let selectedSession = findSession(sessions, authRequest);
// if (!selectedSession) {
// selectedSession = sessions[0]; // TODO: remove
// }
if (selectedSession && selectedSession.id) {
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
const { callbackUrl } = await createCallback(server, {
authRequestId,
session,
});
return NextResponse.redirect(callbackUrl);
} else {
const accountsUrl = new URL("/accounts", request.url);
if (authRequest?.id) {
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
}
return NextResponse.redirect(accountsUrl);
}
} else {
const accountsUrl = new URL("/accounts", request.url);
if (authRequest?.id) {
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
}
return NextResponse.redirect(accountsUrl);
// return NextResponse.error();
}
}
} else {
const loginNameUrl = new URL("/loginname", request.url);
if (authRequest?.id) {
loginNameUrl.searchParams.set("authRequestId", authRequest?.id);
if (authRequest.loginHint) {
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
}
}
return NextResponse.redirect(loginNameUrl);
}
} else {
return NextResponse.error();
}
}

View File

@@ -7,6 +7,7 @@ export default async function Page({
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
}) { }) {
const loginName = searchParams?.loginName; const loginName = searchParams?.loginName;
const authRequestId = searchParams?.authRequestId;
const submit: boolean = searchParams?.submit === "true"; const submit: boolean = searchParams?.submit === "true";
const loginSettings = await getLoginSettings(server); const loginSettings = await getLoginSettings(server);
@@ -19,6 +20,7 @@ export default async function Page({
<UsernameForm <UsernameForm
loginSettings={loginSettings} loginSettings={loginSettings}
loginName={loginName} loginName={loginName}
authRequestId={authRequestId}
submit={submit} submit={submit}
/> />
</div> </div>

View File

@@ -13,7 +13,7 @@ export default async function Page({
}: { }: {
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
}) { }) {
const { loginName, altPassword } = searchParams; const { loginName, altPassword, authRequestId } = searchParams;
const sessionFactors = await loadSession(loginName); const sessionFactors = await loadSession(loginName);
@@ -48,6 +48,7 @@ export default async function Page({
{loginName && ( {loginName && (
<LoginPasskey <LoginPasskey
loginName={loginName} loginName={loginName}
authRequestId={authRequestId}
altPassword={altPassword === "true"} altPassword={altPassword === "true"}
/> />
)} )}

View File

@@ -9,7 +9,7 @@ export default async function Page({
}: { }: {
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
}) { }) {
const { loginName, promptPasswordless, alt } = searchParams; const { loginName, promptPasswordless, authRequestId, alt } = searchParams;
const sessionFactors = await loadSession(loginName); const sessionFactors = await loadSession(loginName);
async function loadSession(loginName?: string) { async function loadSession(loginName?: string) {
@@ -46,6 +46,7 @@ export default async function Page({
<PasswordForm <PasswordForm
loginName={loginName} loginName={loginName}
authRequestId={authRequestId}
promptPasswordless={promptPasswordless === "true"} promptPasswordless={promptPasswordless === "true"}
isAlternative={alt === "true"} isAlternative={alt === "true"}
/> />

View File

@@ -1,10 +1,10 @@
import { ProviderSlug } from "#/lib/demos"; import { ProviderSlug } from "#/lib/demos";
import { addHumanUser, server } from "#/lib/zitadel"; import { server } from "#/lib/zitadel";
import Alert, { AlertType } from "#/ui/Alert"; import Alert, { AlertType } from "#/ui/Alert";
import { import {
AddHumanUserRequest, AddHumanUserRequest,
IDPInformation, IDPInformation,
RetrieveIdentityProviderInformationResponse, RetrieveIdentityProviderIntentResponse,
user, user,
IDPLink, IDPLink,
} from "@zitadel/server"; } from "@zitadel/server";
@@ -27,8 +27,8 @@ const PROVIDER_MAPPING: {
// organisation: Organisation | undefined; // organisation: Organisation | undefined;
profile: { profile: {
displayName: idp.rawInformation?.User?.name ?? "", displayName: idp.rawInformation?.User?.name ?? "",
firstName: idp.rawInformation?.User?.given_name ?? "", givenName: idp.rawInformation?.User?.given_name ?? "",
lastName: idp.rawInformation?.User?.family_name ?? "", familyName: idp.rawInformation?.User?.family_name ?? "",
}, },
idpLinks: [idpLink], idpLinks: [idpLink],
}; };
@@ -49,8 +49,8 @@ const PROVIDER_MAPPING: {
// organisation: Organisation | undefined; // organisation: Organisation | undefined;
profile: { profile: {
displayName: idp.rawInformation?.name ?? "", displayName: idp.rawInformation?.name ?? "",
firstName: idp.rawInformation?.name ?? "", givenName: idp.rawInformation?.name ?? "",
lastName: idp.rawInformation?.name ?? "", familyName: idp.rawInformation?.name ?? "",
}, },
idpLinks: [idpLink], idpLinks: [idpLink],
}; };
@@ -64,8 +64,11 @@ function retrieveIDP(
): Promise<IDPInformation | undefined> { ): Promise<IDPInformation | undefined> {
const userService = user.getUser(server); const userService = user.getUser(server);
return userService return userService
.retrieveIdentityProviderInformation({ intentId: id, token: token }, {}) .retrieveIdentityProviderIntent(
.then((resp: RetrieveIdentityProviderInformationResponse) => { { idpIntentId: id, idpIntentToken: token },
{}
)
.then((resp: RetrieveIdentityProviderIntentResponse) => {
return resp.idpInformation; return resp.idpInformation;
}); });
} }

View File

@@ -1,10 +1,19 @@
import { getSession, server } from "#/lib/zitadel"; import { createCallback, getSession, server } from "#/lib/zitadel";
import UserAvatar from "#/ui/UserAvatar"; import UserAvatar from "#/ui/UserAvatar";
import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; import { getMostRecentCookieWithLoginname } from "#/utils/cookies";
import { redirect } from "next/navigation";
async function loadSession(loginName: string) { async function loadSession(loginName: string, authRequestId?: string) {
const recent = await getMostRecentCookieWithLoginname(`${loginName}`); const recent = await getMostRecentCookieWithLoginname(`${loginName}`);
if (authRequestId) {
return createCallback(server, {
authRequestId,
session: { sessionId: recent.id, sessionToken: recent.token },
}).then(({ callbackUrl }) => {
return redirect(callbackUrl);
});
}
return getSession(server, recent.id, recent.token).then((response) => { return getSession(server, recent.id, recent.token).then((response) => {
if (response?.session) { if (response?.session) {
return response.session; return response.session;
@@ -13,8 +22,8 @@ async function loadSession(loginName: string) {
} }
export default async function Page({ searchParams }: { searchParams: any }) { export default async function Page({ searchParams }: { searchParams: any }) {
const { loginName } = searchParams; const { loginName, authRequestId } = searchParams;
const sessionFactors = await loadSession(loginName); const sessionFactors = await loadSession(loginName, authRequestId);
return ( return (
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">

View File

@@ -6,7 +6,13 @@ export async function POST(request: NextRequest) {
if (body) { if (body) {
let { idpId, successUrl, failureUrl } = body; let { idpId, successUrl, failureUrl } = body;
return startIdentityProviderFlow(server, { idpId, successUrl, failureUrl }) return startIdentityProviderFlow(server, {
idpId,
urls: {
successUrl,
failureUrl,
},
})
.then((resp) => { .then((resp) => {
return NextResponse.json(resp); return NextResponse.json(resp);
}) })

View File

@@ -1,53 +1,18 @@
import { import { listAuthenticationMethodTypes } from "#/lib/zitadel";
getSession,
listAuthenticationMethodTypes,
server,
} from "#/lib/zitadel";
import { getSessionCookieById } from "#/utils/cookies";
import { createSessionAndUpdateCookie } from "#/utils/session"; import { createSessionAndUpdateCookie } from "#/utils/session";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const sessionId = searchParams.get("sessionId");
if (sessionId) {
const sessionCookie = await getSessionCookieById(sessionId);
const session = await getSession(
server,
sessionCookie.id,
sessionCookie.token
);
const userId = session?.session?.factors?.user?.id;
if (userId) {
return listAuthenticationMethodTypes(userId)
.then((methods) => {
return NextResponse.json(methods);
})
.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 });
}
}
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
if (body) { if (body) {
const { loginName } = body; const { loginName, authRequestId } = body;
const domain: string = request.nextUrl.hostname; return createSessionAndUpdateCookie(
loginName,
return createSessionAndUpdateCookie(loginName, undefined, domain, undefined) undefined,
undefined,
authRequestId
)
.then((session) => { .then((session) => {
if (session.factors?.user?.id) { if (session.factors?.user?.id) {
return listAuthenticationMethodTypes(session.factors?.user?.id) return listAuthenticationMethodTypes(session.factors?.user?.id)
@@ -62,16 +27,11 @@ export async function POST(request: NextRequest) {
return NextResponse.json(error, { status: 500 }); return NextResponse.json(error, { status: 500 });
}); });
} else { } else {
throw "No user id found in session"; throw { details: "No user id found in session" };
} }
}) })
.catch((error) => { .catch((error) => {
return NextResponse.json( return NextResponse.json(error, { status: 500 });
{
details: "could not add session to cookie",
},
{ status: 500 }
);
}); });
} else { } else {
return NextResponse.error(); return NextResponse.error();

View File

@@ -10,6 +10,7 @@ import {
createSessionAndUpdateCookie, createSessionAndUpdateCookie,
setSessionAndUpdateCookie, setSessionAndUpdateCookie,
} from "#/utils/session"; } from "#/utils/session";
import { RequestChallenges } from "@zitadel/server";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -17,12 +18,10 @@ export async function POST(request: NextRequest) {
if (body) { if (body) {
const { loginName, password } = body; const { loginName, password } = body;
const domain: string = request.nextUrl.hostname;
return createSessionAndUpdateCookie( return createSessionAndUpdateCookie(
loginName, loginName,
password, password,
domain, undefined,
undefined undefined
).then((session) => { ).then((session) => {
return NextResponse.json(session); return NextResponse.json(session);
@@ -44,7 +43,8 @@ export async function PUT(request: NextRequest) {
const body = await request.json(); const body = await request.json();
if (body) { if (body) {
const { loginName, password, challenges, passkey } = body; const { loginName, password, webAuthN, authRequestId } = body;
const challenges: RequestChallenges = body.challenges;
const recentPromise: Promise<SessionCookie> = loginName const recentPromise: Promise<SessionCookie> = loginName
? getSessionCookieByLoginName(loginName).catch((error) => { ? getSessionCookieByLoginName(loginName).catch((error) => {
@@ -56,16 +56,21 @@ export async function PUT(request: NextRequest) {
const domain: string = request.nextUrl.hostname; const domain: string = request.nextUrl.hostname;
if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) {
challenges.webAuthN.domain = domain;
}
return recentPromise return recentPromise
.then((recent) => { .then((recent) => {
console.log("setsession", webAuthN);
return setSessionAndUpdateCookie( return setSessionAndUpdateCookie(
recent.id, recent.id,
recent.token, recent.token,
recent.loginName, recent.loginName,
password, password,
passkey, webAuthN,
domain, challenges,
challenges authRequestId
).then((session) => { ).then((session) => {
return NextResponse.json({ return NextResponse.json({
sessionId: session.id, sessionId: session.id,

View File

@@ -2,7 +2,7 @@ import { stub } from "../support/mock";
describe("login", () => { describe("login", () => {
beforeEach(() => { beforeEach(() => {
stub("zitadel.session.v2alpha.SessionService", "CreateSession", { stub("zitadel.session.v2beta.SessionService", "CreateSession", {
data: { data: {
details: { details: {
sequence: 859, sequence: 859,
@@ -16,7 +16,7 @@ describe("login", () => {
}, },
}); });
stub("zitadel.session.v2alpha.SessionService", "GetSession", { stub("zitadel.session.v2beta.SessionService", "GetSession", {
data: { data: {
session: { session: {
id: "221394658884845598", id: "221394658884845598",
@@ -29,16 +29,15 @@ describe("login", () => {
loginName: "john@zitadel.com", loginName: "john@zitadel.com",
}, },
password: undefined, password: undefined,
passkey: undefined, webAuthN: undefined,
intent: undefined, intent: undefined,
}, },
metadata: {}, metadata: {},
domain: "localhost",
}, },
}, },
}); });
stub("zitadel.settings.v2alpha.SettingsService", "GetLoginSettings", { stub("zitadel.settings.v2beta.SettingsService", "GetLoginSettings", {
data: { data: {
settings: { settings: {
passkeysType: 1, passkeysType: 1,
@@ -48,23 +47,19 @@ describe("login", () => {
}); });
describe("password login", () => { describe("password login", () => {
beforeEach(() => { beforeEach(() => {
stub( stub("zitadel.user.v2beta.UserService", "ListAuthenticationMethodTypes", {
"zitadel.user.v2alpha.UserService", data: {
"ListAuthenticationMethodTypes", authMethodTypes: [1], // 1 for password authentication
{ },
data: { });
authMethodTypes: [1], // 1 for password authentication
},
}
);
}); });
it("should redirect a user with password authentication to /password", () => { it("should redirect a user with password authentication to /password", () => {
cy.visit("/loginname?loginName=johndoe%40zitadel.com&submit=true"); cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/password"); cy.location("pathname", { timeout: 10_000 }).should("eq", "/password");
}); });
describe("with passkey prompt", () => { describe("with passkey prompt", () => {
beforeEach(() => { beforeEach(() => {
stub("zitadel.session.v2alpha.SessionService", "SetSession", { stub("zitadel.session.v2beta.SessionService", "SetSession", {
data: { data: {
details: { details: {
sequence: 859, sequence: 859,
@@ -91,18 +86,14 @@ describe("login", () => {
}); });
describe("passkey login", () => { describe("passkey login", () => {
beforeEach(() => { beforeEach(() => {
stub( stub("zitadel.user.v2beta.UserService", "ListAuthenticationMethodTypes", {
"zitadel.user.v2alpha.UserService", data: {
"ListAuthenticationMethodTypes", authMethodTypes: [2], // 2 for passwordless authentication
{ },
data: { });
authMethodTypes: [2], // 2 for passwordless authentication
},
}
);
}); });
it("should redirect a user with passwordless authentication to /passkey/login", () => { it("should redirect a user with passwordless authentication to /passkey/login", () => {
cy.visit("/loginname?loginName=johndoe%40zitadel.com&submit=true"); cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
cy.location("pathname", { timeout: 10_000 }).should( cy.location("pathname", { timeout: 10_000 }).should(
"eq", "eq",
"/passkey/login" "/passkey/login"

View File

@@ -4,7 +4,7 @@ const IDP_URL = "https://example.com/idp/url";
describe("register idps", () => { describe("register idps", () => {
beforeEach(() => { beforeEach(() => {
stub("zitadel.user.v2alpha.UserService", "StartIdentityProviderFlow", { stub("zitadel.user.v2beta.UserService", "StartIdentityProviderIntent", {
data: { data: {
authUrl: IDP_URL, authUrl: IDP_URL,
}, },

View File

@@ -2,7 +2,7 @@ import { stub } from "../support/mock";
describe("register", () => { describe("register", () => {
beforeEach(() => { beforeEach(() => {
stub("zitadel.user.v2alpha.UserService", "AddHumanUser", { stub("zitadel.user.v2beta.UserService", "AddHumanUser", {
data: { data: {
userId: "123", userId: "123",
}, },

View File

@@ -2,12 +2,12 @@ import { stub } from "../support/mock";
describe("/verify", () => { describe("/verify", () => {
it("redirects after successful email verification", () => { it("redirects after successful email verification", () => {
stub("zitadel.user.v2alpha.UserService", "VerifyEmail"); stub("zitadel.user.v2beta.UserService", "VerifyEmail");
cy.visit("/verify?userID=123&code=abc&submit=true"); cy.visit("/verify?userID=123&code=abc&submit=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/loginname"); cy.location("pathname", { timeout: 10_000 }).should("eq", "/loginname");
}); });
it("shows an error if validation failed", () => { it("shows an error if validation failed", () => {
stub("zitadel.user.v2alpha.UserService", "VerifyEmail", { stub("zitadel.user.v2beta.UserService", "VerifyEmail", {
code: 3, code: 3,
error: "error validating code", error: "error validating code",
}); });

View File

@@ -2,6 +2,7 @@ import {
ZitadelServer, ZitadelServer,
ZitadelServerOptions, ZitadelServerOptions,
user, user,
oidc,
settings, settings,
getServers, getServers,
initializeServer, initializeServer,
@@ -19,16 +20,22 @@ import {
GetSessionResponse, GetSessionResponse,
VerifyEmailResponse, VerifyEmailResponse,
SetSessionResponse, SetSessionResponse,
SetSessionRequest,
DeleteSessionResponse, DeleteSessionResponse,
VerifyPasskeyRegistrationResponse, VerifyPasskeyRegistrationResponse,
ChallengeKind,
LoginSettings, LoginSettings,
GetLoginSettingsResponse, GetLoginSettingsResponse,
ListAuthenticationMethodTypesResponse, ListAuthenticationMethodTypesResponse,
StartIdentityProviderFlowRequest, StartIdentityProviderIntentRequest,
StartIdentityProviderFlowResponse, StartIdentityProviderIntentResponse,
RetrieveIdentityProviderInformationRequest, RetrieveIdentityProviderIntentRequest,
RetrieveIdentityProviderInformationResponse, RetrieveIdentityProviderIntentResponse,
GetAuthRequestResponse,
GetAuthRequestRequest,
CreateCallbackRequest,
CreateCallbackResponse,
RequestChallenges,
AddHumanUserRequest,
} from "@zitadel/server"; } from "@zitadel/server";
export const zitadelConfig: ZitadelServerOptions = { export const zitadelConfig: ZitadelServerOptions = {
@@ -95,9 +102,8 @@ export async function getPasswordComplexitySettings(
export async function createSession( export async function createSession(
server: ZitadelServer, server: ZitadelServer,
loginName: string, loginName: string,
domain: string,
password: string | undefined, password: string | undefined,
challenges: ChallengeKind[] | undefined challenges: RequestChallenges | undefined
): Promise<CreateSessionResponse | undefined> { ): Promise<CreateSessionResponse | undefined> {
const sessionService = session.getSession(server); const sessionService = session.getSession(server);
return password return password
@@ -105,12 +111,12 @@ export async function createSession(
{ {
checks: { user: { loginName }, password: { password } }, checks: { user: { loginName }, password: { password } },
challenges, challenges,
domain,
}, },
{} {}
) )
: sessionService.createSession( : sessionService.createSession(
{ checks: { user: { loginName } }, domain }, { checks: { user: { loginName } }, challenges },
{} {}
); );
} }
@@ -119,23 +125,29 @@ export async function setSession(
server: ZitadelServer, server: ZitadelServer,
sessionId: string, sessionId: string,
sessionToken: string, sessionToken: string,
domain: string | undefined,
password: string | undefined, password: string | undefined,
passkey: { credentialAssertionData: any } | undefined, webAuthN: { credentialAssertionData: any } | undefined,
challenges: ChallengeKind[] | undefined challenges: RequestChallenges | undefined
): Promise<SetSessionResponse | undefined> { ): Promise<SetSessionResponse | undefined> {
const sessionService = session.getSession(server); const sessionService = session.getSession(server);
const payload = { sessionId, sessionToken, challenges, domain }; const payload: SetSessionRequest = {
return password sessionId,
? sessionService.setSession( sessionToken,
{ challenges,
...payload, checks: {},
checks: { password: { password }, passkey }, metadata: {},
}, };
{}
) if (password && payload.checks) {
: sessionService.setSession(payload, {}); payload.checks.password = { password };
}
if (webAuthN && payload.checks) {
payload.checks.webAuthN = webAuthN;
}
return sessionService.setSession(payload, {});
} }
export async function getSession( export async function getSession(
@@ -179,10 +191,10 @@ export async function addHumanUser(
): Promise<string> { ): Promise<string> {
const userService = user.getUser(server); const userService = user.getUser(server);
const payload = { const payload: Partial<AddHumanUserRequest> = {
email: { email }, email: { email },
username: email, username: email,
profile: { firstName, lastName }, profile: { givenName: firstName, familyName: lastName },
}; };
return userService return userService
.addHumanUser( .addHumanUser(
@@ -201,29 +213,48 @@ export async function addHumanUser(
export async function startIdentityProviderFlow( export async function startIdentityProviderFlow(
server: ZitadelServer, server: ZitadelServer,
{ idpId, successUrl, failureUrl }: StartIdentityProviderFlowRequest { idpId, urls }: StartIdentityProviderIntentRequest
): Promise<StartIdentityProviderFlowResponse> { ): Promise<StartIdentityProviderIntentResponse> {
const userService = user.getUser(server); const userService = user.getUser(server);
return userService.startIdentityProviderFlow({ return userService.startIdentityProviderIntent({
idpId, idpId,
successUrl, urls,
failureUrl,
}); });
} }
export async function retrieveIdentityProviderInformation( export async function retrieveIdentityProviderInformation(
server: ZitadelServer, server: ZitadelServer,
{ intentId, token }: RetrieveIdentityProviderInformationRequest { idpIntentId, idpIntentToken }: RetrieveIdentityProviderIntentRequest
): Promise<RetrieveIdentityProviderInformationResponse> { ): Promise<RetrieveIdentityProviderIntentResponse> {
const userService = user.getUser(server); const userService = user.getUser(server);
return userService.retrieveIdentityProviderInformation({ return userService.retrieveIdentityProviderIntent({
intentId, idpIntentId,
token, idpIntentToken,
}); });
} }
export async function getAuthRequest(
server: ZitadelServer,
{ authRequestId }: GetAuthRequestRequest
): Promise<GetAuthRequestResponse> {
const oidcService = oidc.getOidc(server);
return oidcService.getAuthRequest({
authRequestId,
});
}
export async function createCallback(
server: ZitadelServer,
req: CreateCallbackRequest
): Promise<CreateCallbackResponse> {
const oidcService = oidc.getOidc(server);
return oidcService.createCallback(req);
}
export async function verifyEmail( export async function verifyEmail(
server: ZitadelServer, server: ZitadelServer,
userId: string, userId: string,

28
apps/login/middleware.ts Normal file
View File

@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export const config = {
matcher: ["/.well-known/:path*", "/oauth/:path*", "/oidc/:path*"],
};
const INSTANCE = process.env.ZITADEL_API_URL;
const SERVICE_USER_ID = process.env.ZITADEL_SERVICE_USER_ID as string;
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-zitadel-login-client", SERVICE_USER_ID);
requestHeaders.set("Forwarded", `host="${request.nextUrl.host}"`);
const responseHeaders = new Headers();
responseHeaders.set("Access-Control-Allow-Origin", "*");
responseHeaders.set("Access-Control-Allow-Headers", "*");
request.nextUrl.href = `${INSTANCE}${request.nextUrl.pathname}${request.nextUrl.search}`;
return NextResponse.rewrite(request.nextUrl, {
request: {
headers: requestHeaders,
},
headers: responseHeaders,
});
}

View File

@@ -1,11 +1,11 @@
[ [
{ {
"service": "zitadel.settings.v2alpha.SettingsService", "service": "zitadel.settings.v2beta.SettingsService",
"method": "GetBrandingSettings", "method": "GetBrandingSettings",
"out": {} "out": {}
}, },
{ {
"service": "zitadel.settings.v2alpha.SettingsService", "service": "zitadel.settings.v2beta.SettingsService",
"method": "GetLegalAndSupportSettings", "method": "GetLegalAndSupportSettings",
"out": { "out": {
"data": { "data": {
@@ -18,7 +18,7 @@
} }
}, },
{ {
"service": "zitadel.settings.v2alpha.SettingsService", "service": "zitadel.settings.v2beta.SettingsService",
"method": "GetActiveIdentityProviders", "method": "GetActiveIdentityProviders",
"out": { "out": {
"data": { "data": {
@@ -33,7 +33,7 @@
} }
}, },
{ {
"service": "zitadel.settings.v2alpha.SettingsService", "service": "zitadel.settings.v2beta.SettingsService",
"method": "GetPasswordComplexitySettings", "method": "GetPasswordComplexitySettings",
"out": { "out": {
"data": { "data": {

View File

@@ -1,6 +1,6 @@
zitadel/user/v2alpha/user_service.proto zitadel/user/v2beta/user_service.proto
zitadel/session/v2alpha/session_service.proto zitadel/session/v2beta/session_service.proto
zitadel/settings/v2alpha/settings_service.proto zitadel/settings/v2beta/settings_service.proto
zitadel/management.proto zitadel/management.proto
zitadel/auth.proto zitadel/auth.proto
zitadel/admin.proto zitadel/admin.proto

View File

@@ -40,7 +40,6 @@
"@zitadel/react": "workspace:*", "@zitadel/react": "workspace:*",
"@zitadel/server": "workspace:*", "@zitadel/server": "workspace:*",
"clsx": "1.2.1", "clsx": "1.2.1",
"date-fns": "2.29.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "13.4.12", "next": "13.4.12",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",

View File

@@ -2,7 +2,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Challenges_Passkey } from "@zitadel/server";
import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
import { Button, ButtonVariants } from "./Button"; import { Button, ButtonVariants } from "./Button";
import Alert from "./Alert"; import Alert from "./Alert";
@@ -10,10 +9,15 @@ import { Spinner } from "./Spinner";
type Props = { type Props = {
loginName: string; loginName: string;
authRequestId?: string;
altPassword: boolean; altPassword: boolean;
}; };
export default function LoginPasskey({ loginName, altPassword }: Props) { export default function LoginPasskey({
loginName,
authRequestId,
altPassword,
}: Props) {
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -28,7 +32,7 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
updateSessionForChallenge() updateSessionForChallenge()
.then((response) => { .then((response) => {
const pK = const pK =
response.challenges.passkey.publicKeyCredentialRequestOptions response.challenges.webAuthN.publicKeyCredentialRequestOptions
.publicKey; .publicKey;
if (pK) { if (pK) {
submitLoginAndContinue(pK) submitLoginAndContinue(pK)
@@ -60,7 +64,13 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName, loginName,
challenges: [1], // request passkey challenge challenges: {
webAuthN: {
domain: "",
userVerificationRequirement: 1,
},
},
authRequestId,
}), }),
}); });
@@ -81,7 +91,8 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName, loginName,
passkey: data, webAuthN: { credentialAssertionData: data },
authRequestId,
}), }),
}); });
@@ -115,18 +126,18 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
}) })
.then((assertedCredential: any) => { .then((assertedCredential: any) => {
if (assertedCredential) { if (assertedCredential) {
let authData = new Uint8Array( const authData = new Uint8Array(
assertedCredential.response.authenticatorData assertedCredential.response.authenticatorData
); );
let clientDataJSON = new Uint8Array( const clientDataJSON = new Uint8Array(
assertedCredential.response.clientDataJSON assertedCredential.response.clientDataJSON
); );
let rawId = new Uint8Array(assertedCredential.rawId); const rawId = new Uint8Array(assertedCredential.rawId);
let sig = new Uint8Array(assertedCredential.response.signature); const sig = new Uint8Array(assertedCredential.response.signature);
let userHandle = new Uint8Array( const userHandle = new Uint8Array(
assertedCredential.response.userHandle assertedCredential.response.userHandle
); );
let data = JSON.stringify({ const data = {
id: assertedCredential.id, id: assertedCredential.id,
rawId: coerceToBase64Url(rawId, "rawId"), rawId: coerceToBase64Url(rawId, "rawId"),
type: assertedCredential.type, type: assertedCredential.type,
@@ -139,9 +150,21 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
signature: coerceToBase64Url(sig, "sig"), signature: coerceToBase64Url(sig, "sig"),
userHandle: coerceToBase64Url(userHandle, "userHandle"), userHandle: coerceToBase64Url(userHandle, "userHandle"),
}, },
}); };
return submitLogin(data).then(() => { return submitLogin(data).then((resp) => {
return router.push(`/accounts`); return router.push(
`/signedin?` +
new URLSearchParams(
authRequestId
? {
loginName: resp.factors.user.loginName,
authRequestId,
}
: {
loginName: resp.factors.user.loginName,
}
)
);
}); });
} else { } else {
setLoading(false); setLoading(false);
@@ -169,11 +192,16 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
<Button <Button
type="button" type="button"
variant={ButtonVariants.Secondary} variant={ButtonVariants.Secondary}
onClick={() => onClick={() => {
router.push( const params = { loginName, alt: "true" };
"/password?" + new URLSearchParams({ loginName, alt: "true" }) // alt is set because password is requested as alternative auth method, so passwordless prompt can be escaped
) return router.push(
} "/password?" +
new URLSearchParams(
authRequestId ? { ...params, authRequestId } : params
) // alt is set because password is requested as alternative auth method, so passwordless prompt can be escaped
);
}}
> >
use password use password
</Button> </Button>

View File

@@ -14,12 +14,14 @@ type Inputs = {
type Props = { type Props = {
loginName?: string; loginName?: string;
authRequestId?: string;
isAlternative?: boolean; // whether password was requested as alternative auth method isAlternative?: boolean; // whether password was requested as alternative auth method
promptPasswordless?: boolean; promptPasswordless?: boolean;
}; };
export default function PasswordForm({ export default function PasswordForm({
loginName, loginName,
authRequestId,
promptPasswordless, promptPasswordless,
isAlternative, isAlternative,
}: Props) { }: Props) {
@@ -44,6 +46,7 @@ export default function PasswordForm({
body: JSON.stringify({ body: JSON.stringify({
loginName, loginName,
password: values.password, password: values.password,
authRequestId,
}), }),
}); });
@@ -73,7 +76,19 @@ export default function PasswordForm({
}) })
); );
} else { } else {
return router.push(`/accounts`); return router.push(
`/signedin?` +
new URLSearchParams(
authRequestId
? {
loginName: resp.factors.user.loginName,
authRequestId,
}
: {
loginName: resp.factors.user.loginName,
}
)
);
} }
}); });
} }

View File

@@ -66,6 +66,7 @@ export default function RegisterFormWithoutPassword({ legal }: Props) {
}, },
body: JSON.stringify({ body: JSON.stringify({
loginName: loginName, loginName: loginName,
// authRequestId, register does not need an oidc callback at the end
}), }),
}); });

View File

@@ -9,9 +9,11 @@ import { XCircleIcon } from "@heroicons/react/24/outline";
export default function SessionItem({ export default function SessionItem({
session, session,
reload, reload,
authRequestId,
}: { }: {
session: Session; session: Session;
reload: () => void; reload: () => void;
authRequestId?: string;
}) { }) {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -39,7 +41,7 @@ export default function SessionItem({
} }
const validPassword = session?.factors?.password?.verifiedAt; const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.passkey?.verifiedAt; const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validUser = validPassword || validPasskey; const validUser = validPassword || validPasskey;
@@ -48,14 +50,29 @@ export default function SessionItem({
href={ href={
validUser validUser
? `/signedin?` + ? `/signedin?` +
new URLSearchParams({ new URLSearchParams(
loginName: session.factors?.user?.loginName as string, authRequestId
}) ? {
loginName: session.factors?.user?.loginName as string,
authRequestId,
}
: {
loginName: session.factors?.user?.loginName as string,
}
)
: `/loginname?` + : `/loginname?` +
new URLSearchParams({ new URLSearchParams(
loginName: session.factors?.user?.loginName as string, authRequestId
submit: "true", ? {
}) loginName: session.factors?.user?.loginName as string,
submit: "true",
authRequestId,
}
: {
loginName: session.factors?.user?.loginName as string,
submit: "true",
}
)
} }
className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all" className="group flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light hover:shadow-lg dark:hover:bg-white/10 py-2 px-4 rounded-md transition-all"
> >

View File

@@ -7,9 +7,10 @@ import { useEffect, useState } from "react";
type Props = { type Props = {
sessions: Session[]; sessions: Session[];
authRequestId?: string;
}; };
export default function SessionsList({ sessions }: Props) { export default function SessionsList({ sessions, authRequestId }: Props) {
const [list, setList] = useState<Session[]>(sessions); const [list, setList] = useState<Session[]>(sessions);
return sessions ? ( return sessions ? (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
@@ -19,6 +20,7 @@ export default function SessionsList({ sessions }: Props) {
return ( return (
<SessionItem <SessionItem
session={session} session={session}
authRequestId={authRequestId}
reload={() => { reload={() => {
setList(list.filter((s) => s.id !== session.id)); setList(list.filter((s) => s.id !== session.id));
}} }}

View File

@@ -79,6 +79,7 @@ export default function SetPasswordForm({
body: JSON.stringify({ body: JSON.stringify({
loginName: loginName, loginName: loginName,
password: password, password: password,
// authRequestId, register does not need an oidc callback
}), }),
}); });

View File

@@ -19,7 +19,7 @@ export interface SignInWithIDPProps {
} }
const START_IDP_FLOW_PATH = (idpId: string) => const START_IDP_FLOW_PATH = (idpId: string) =>
`/v2alpha/users/idps/${idpId}/start`; `/v2beta/users/idps/${idpId}/start`;
export function SignInWithIDP({ export function SignInWithIDP({
host, host,

View File

@@ -7,6 +7,7 @@ import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Spinner } from "./Spinner"; import { Spinner } from "./Spinner";
import { LoginSettings } from "@zitadel/server"; import { LoginSettings } from "@zitadel/server";
import Alert from "./Alert";
type Inputs = { type Inputs = {
loginName: string; loginName: string;
@@ -15,12 +16,14 @@ type Inputs = {
type Props = { type Props = {
loginSettings: LoginSettings | undefined; loginSettings: LoginSettings | undefined;
loginName: string | undefined; loginName: string | undefined;
authRequestId: string | undefined;
submit: boolean; submit: boolean;
}; };
export default function UsernameForm({ export default function UsernameForm({
loginSettings, loginSettings,
loginName, loginName,
authRequestId,
submit, submit,
}: Props) { }: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({ const { register, handleSubmit, formState } = useForm<Inputs>({
@@ -37,19 +40,25 @@ export default function UsernameForm({
async function submitLoginName(values: Inputs) { async function submitLoginName(values: Inputs) {
setLoading(true); setLoading(true);
const body = {
loginName: values.loginName,
};
const res = await fetch("/api/loginname", { const res = await fetch("/api/loginname", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify(authRequestId ? { ...body, authRequestId } : body),
loginName: values.loginName,
}),
}); });
setLoading(false); setLoading(false);
if (!res.ok) { if (!res.ok) {
throw new Error("Failed to load authentication methods"); const response = await res.json();
setError(response.details);
return Promise.reject(response.details);
} }
return res.json(); return res.json();
} }
@@ -60,33 +69,40 @@ export default function UsernameForm({
const method = response.authMethodTypes[0]; const method = response.authMethodTypes[0];
switch (method) { switch (method) {
case 1: //AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSWORD: case 1: //AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSWORD:
const paramsPassword: any = { loginName: values.loginName };
if (loginSettings?.passkeysType === 1) {
paramsPassword.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
if (authRequestId) {
paramsPassword.authRequestId = authRequestId;
}
return router.push( return router.push(
"/password?" + "/password?" + new URLSearchParams(paramsPassword)
new URLSearchParams(
loginSettings?.passkeysType === 1
? {
loginName: values.loginName,
promptPasswordless: `true`, // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
: { loginName: values.loginName }
)
); );
case 2: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY case 2: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
const paramsPasskey: any = { loginName: values.loginName };
if (authRequestId) {
paramsPasskey.authRequestId = authRequestId;
}
return router.push( return router.push(
"/passkey/login?" + "/passkey/login?" + new URLSearchParams(paramsPasskey)
new URLSearchParams({ loginName: values.loginName })
); );
default: default:
const paramsPasskeyDefault: any = { loginName: values.loginName };
if (loginSettings?.passkeysType === 1) {
paramsPasskeyDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
if (authRequestId) {
paramsPasskeyDefault.authRequestId = authRequestId;
}
return router.push( return router.push(
"/password?" + "/password?" + new URLSearchParams(paramsPasskeyDefault)
new URLSearchParams(
loginSettings?.passkeysType === 1
? {
loginName: values.loginName,
promptPasswordless: `true`, // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
: { loginName: values.loginName }
)
); );
} }
} else if ( } else if (
@@ -99,12 +115,17 @@ export default function UsernameForm({
} else { } else {
// prefer passkey in favor of other methods // prefer passkey in favor of other methods
if (response.authMethodTypes.includes(2)) { if (response.authMethodTypes.includes(2)) {
const passkeyParams: any = {
loginName: values.loginName,
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
};
if (authRequestId) {
passkeyParams.authRequestId = authRequestId;
}
return router.push( return router.push(
"/passkey/login?" + "/passkey/login?" + new URLSearchParams(passkeyParams)
new URLSearchParams({
loginName: values.loginName,
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
})
); );
} }
} }
@@ -128,14 +149,16 @@ export default function UsernameForm({
autoComplete="username" autoComplete="username"
{...register("loginName", { required: "This field is required" })} {...register("loginName", { required: "This field is required" })}
label="Loginname" label="Loginname"
// error={errors.username?.message as string}
/> />
</div> </div>
{error && (
<div className="py-4">
<Alert>{error}</Alert>
</div>
)}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">
{/* <Button type="button" variant={ButtonVariants.Secondary}>
back
</Button> */}
<span className="flex-grow"></span> <span className="flex-grow"></span>
<Button <Button
type="submit" type="submit"

View File

@@ -7,6 +7,7 @@ export type SessionCookie = {
token: string; token: string;
loginName: string; loginName: string;
changeDate: string; changeDate: string;
authRequestId?: string; // if its linked to an OIDC flow
}; };
function setSessionHttpOnlyCookie(sessions: SessionCookie[]) { function setSessionHttpOnlyCookie(sessions: SessionCookie[]) {
@@ -134,7 +135,7 @@ export async function getSessionCookieByLoginName(
} }
} }
export async function getAllSessionIds(): Promise<any> { export async function getAllSessionCookieIds(): Promise<any> {
const cookiesList = cookies(); const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions"); const stringifiedCookie = cookiesList.get("sessions");
@@ -146,6 +147,18 @@ export async function getAllSessionIds(): Promise<any> {
} }
} }
export async function getAllSessions(): Promise<SessionCookie[]> {
const cookiesList = cookies();
const stringifiedCookie = cookiesList.get("sessions");
if (stringifiedCookie?.value) {
const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value);
return sessions;
} else {
return [];
}
}
/** /**
* Returns most recent session filtered by optinal loginName * Returns most recent session filtered by optinal loginName
* @param loginName * @param loginName

View File

@@ -4,18 +4,17 @@ import {
addSessionToCookie, addSessionToCookie,
updateSessionCookie, updateSessionCookie,
} from "./cookies"; } from "./cookies";
import { ChallengeKind, Session, Challenges } from "@zitadel/server"; import { Session, Challenges, RequestChallenges } from "@zitadel/server";
export async function createSessionAndUpdateCookie( export async function createSessionAndUpdateCookie(
loginName: string, loginName: string,
password: string | undefined, password: string | undefined,
domain: string, challenges: RequestChallenges | undefined,
challenges: ChallengeKind[] | undefined authRequestId: string | undefined
): Promise<Session> { ): Promise<Session> {
const createdSession = await createSession( const createdSession = await createSession(
server, server,
loginName, loginName,
domain,
password, password,
challenges challenges
); );
@@ -34,6 +33,10 @@ export async function createSessionAndUpdateCookie(
loginName: response.session?.factors?.user?.loginName ?? "", loginName: response.session?.factors?.user?.loginName ?? "",
}; };
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
}
return addSessionToCookie(sessionCookie).then(() => { return addSessionToCookie(sessionCookie).then(() => {
return response.session as Session; return response.session as Session;
}); });
@@ -55,17 +58,16 @@ export async function setSessionAndUpdateCookie(
sessionToken: string, sessionToken: string,
loginName: string, loginName: string,
password: string | undefined, password: string | undefined,
passkey: { credentialAssertionData: any } | undefined, webAuthN: { credentialAssertionData: any } | undefined,
domain: string | undefined, challenges: RequestChallenges | undefined,
challenges: ChallengeKind[] | undefined authRequestId: string | undefined
): Promise<SessionWithChallenges> { ): Promise<SessionWithChallenges> {
return setSession( return setSession(
server, server,
sessionId, sessionId,
sessionToken, sessionToken,
domain,
password, password,
passkey, webAuthN,
challenges challenges
).then((updatedSession) => { ).then((updatedSession) => {
if (updatedSession) { if (updatedSession) {
@@ -76,24 +78,40 @@ export async function setSessionAndUpdateCookie(
loginName: loginName, loginName: loginName,
}; };
return getSession(server, sessionCookie.id, sessionCookie.token).then( if (authRequestId) {
(response) => { sessionCookie.authRequestId = authRequestId;
if (response?.session && response.session.factors?.user?.loginName) { }
const { session } = response;
const newCookie: SessionCookie = {
id: sessionCookie.id,
token: updatedSession.sessionToken,
changeDate: session.changeDate?.toString() ?? "",
loginName: session.factors?.user?.loginName ?? "",
};
return updateSessionCookie(sessionCookie.id, newCookie).then(() => { return new Promise((resolve) => setTimeout(resolve, 1000)).then(() =>
return { challenges: updatedSession.challenges, ...session }; // TODO: remove
}); getSession(server, sessionCookie.id, sessionCookie.token).then(
} else { (response) => {
throw "could not get session or session does not have loginName"; if (
response?.session &&
response.session.factors?.user?.loginName
) {
const { session } = response;
const newCookie: SessionCookie = {
id: sessionCookie.id,
token: updatedSession.sessionToken,
changeDate: session.changeDate?.toString() ?? "",
loginName: session.factors?.user?.loginName ?? "",
};
if (sessionCookie.authRequestId) {
newCookie.authRequestId = sessionCookie.authRequestId;
}
return updateSessionCookie(sessionCookie.id, newCookie).then(
() => {
return { challenges: updatedSession.challenges, ...session };
}
);
} else {
throw "could not get session or session does not have loginName";
}
} }
} )
); );
} else { } else {
throw "Session not be set"; throw "Session not be set";

View File

@@ -18,7 +18,7 @@
"test:unit:watch": "jest --watch", "test:unit:watch": "jest --watch",
"dev": "tsup --watch --dts", "dev": "tsup --watch --dts",
"lint": "eslint \"src/**/*.ts*\"", "lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf src/proto"
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/buf": "^1.14.0", "@bufbuild/buf": "^1.14.0",

View File

@@ -19,7 +19,7 @@
"test:unit:watch": "jest --watch", "test:unit:watch": "jest --watch",
"dev": "tsup --dts --watch", "dev": "tsup --dts --watch",
"lint": "eslint \"src/**/*.ts*\"", "lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf src/proto"
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/buf": "^1.14.0", "@bufbuild/buf": "^1.14.0",

View File

@@ -1,44 +1,58 @@
import * as settings from "./v2/settings"; import * as settings from "./v2/settings";
import * as session from "./v2/session"; import * as session from "./v2/session";
import * as user from "./v2/user"; import * as user from "./v2/user";
import * as oidc from "./v2/oidc";
import * as management from "./management"; import * as management from "./management";
import * as login from "./proto/server/zitadel/settings/v2alpha/login_settings"; import * as login from "./proto/server/zitadel/settings/v2beta/login_settings";
import * as password from "./proto/server/zitadel/settings/v2alpha/password_settings"; import * as password from "./proto/server/zitadel/settings/v2beta/password_settings";
import * as legal from "./proto/server/zitadel/settings/v2alpha/legal_settings"; import * as legal from "./proto/server/zitadel/settings/v2beta/legal_settings";
export { export {
BrandingSettings, BrandingSettings,
Theme, Theme,
} from "./proto/server/zitadel/settings/v2alpha/branding_settings"; } from "./proto/server/zitadel/settings/v2beta/branding_settings";
export { export {
LoginSettings, LoginSettings,
IdentityProvider, IdentityProvider,
IdentityProviderType, IdentityProviderType,
} from "./proto/server/zitadel/settings/v2alpha/login_settings"; } from "./proto/server/zitadel/settings/v2beta/login_settings";
export { export {
ChallengeKind, RequestChallenges,
Challenges, Challenges,
Challenges_Passkey, Challenges_WebAuthN,
} from "./proto/server/zitadel/session/v2alpha/challenge"; } from "./proto/server/zitadel/session/v2beta/challenge";
export {
GetAuthRequestRequest,
GetAuthRequestResponse,
CreateCallbackRequest,
CreateCallbackResponse,
} from "./proto/server/zitadel/oidc/v2beta/oidc_service";
export {
AuthRequest,
Prompt,
} from "./proto/server/zitadel/oidc/v2beta/authorization";
export { export {
Session, Session,
Factors, Factors,
} from "./proto/server/zitadel/session/v2alpha/session"; } from "./proto/server/zitadel/session/v2beta/session";
export { export {
IDPInformation, IDPInformation,
IDPLink, IDPLink,
} from "./proto/server/zitadel/user/v2alpha/idp"; } from "./proto/server/zitadel/user/v2beta/idp";
export { export {
ListSessionsResponse, ListSessionsResponse,
GetSessionResponse, GetSessionResponse,
CreateSessionResponse, CreateSessionResponse,
SetSessionResponse, SetSessionResponse,
SetSessionRequest,
DeleteSessionResponse, DeleteSessionResponse,
} from "./proto/server/zitadel/session/v2alpha/session_service"; } from "./proto/server/zitadel/session/v2beta/session_service";
export { export {
GetPasswordComplexitySettingsResponse, GetPasswordComplexitySettingsResponse,
GetBrandingSettingsResponse, GetBrandingSettingsResponse,
@@ -48,7 +62,7 @@ export {
GetLoginSettingsRequest, GetLoginSettingsRequest,
GetActiveIdentityProvidersResponse, GetActiveIdentityProvidersResponse,
GetActiveIdentityProvidersRequest, GetActiveIdentityProvidersRequest,
} from "./proto/server/zitadel/settings/v2alpha/settings_service"; } from "./proto/server/zitadel/settings/v2beta/settings_service";
export { export {
AddHumanUserResponse, AddHumanUserResponse,
AddHumanUserRequest, AddHumanUserRequest,
@@ -62,19 +76,19 @@ export {
ListAuthenticationMethodTypesResponse, ListAuthenticationMethodTypesResponse,
ListAuthenticationMethodTypesRequest, ListAuthenticationMethodTypesRequest,
AuthenticationMethodType, AuthenticationMethodType,
StartIdentityProviderFlowRequest, StartIdentityProviderIntentRequest,
StartIdentityProviderFlowResponse, StartIdentityProviderIntentResponse,
RetrieveIdentityProviderInformationRequest, RetrieveIdentityProviderIntentRequest,
RetrieveIdentityProviderInformationResponse, RetrieveIdentityProviderIntentResponse,
} from "./proto/server/zitadel/user/v2alpha/user_service"; } from "./proto/server/zitadel/user/v2beta/user_service";
export { export {
SetHumanPasswordResponse, SetHumanPasswordResponse,
SetHumanPasswordRequest, SetHumanPasswordRequest,
} from "./proto/server/zitadel/management"; } from "./proto/server/zitadel/management";
export * from "./proto/server/zitadel/idp"; export * from "./proto/server/zitadel/idp";
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings"; export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2beta/legal_settings";
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2alpha/password_settings"; export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2beta/password_settings";
export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2alpha/settings"; export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2beta/settings";
import { import {
getServers, getServers,
@@ -96,4 +110,5 @@ export {
login, login,
password, password,
legal, legal,
oidc,
}; };

View File

@@ -0,0 +1,2 @@
export * from "./oidc";
export * from "../../proto/server/zitadel/oidc/v2beta/oidc_service";

View File

@@ -0,0 +1,24 @@
import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import { ZitadelServer, createClient, getServers } from "../../server";
import { OIDCServiceClient, OIDCServiceDefinition } from ".";
export const getOidc = (server?: string | ZitadelServer) => {
let config;
if (server && typeof server === "string") {
const apps = getServers();
config = apps.find((a) => a.name === server)?.config;
} else if (server && typeof server === "object") {
config = server.config;
}
if (!config) {
throw Error("No ZITADEL server found");
}
return createClient<OIDCServiceClient>(
OIDCServiceDefinition as CompatServiceDefinition,
config.apiUrl,
config.token
);
};

View File

@@ -1,2 +1,2 @@
export * from "./session"; export * from "./session";
export * from "../../proto/server/zitadel/session/v2alpha/session"; export * from "../../proto/server/zitadel/session/v2beta/session";

View File

@@ -3,7 +3,7 @@ import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import { import {
SessionServiceClient, SessionServiceClient,
SessionServiceDefinition, SessionServiceDefinition,
} from "../../proto/server/zitadel/session/v2alpha/session_service"; } from "../../proto/server/zitadel/session/v2beta/session_service";
import { ZitadelServer, createClient, getServers } from "../../server"; import { ZitadelServer, createClient, getServers } from "../../server";

View File

@@ -1,2 +1,2 @@
export * from "./settings"; export * from "./settings";
export * from "../../proto/server/zitadel/settings/v2alpha/settings"; export * from "../../proto/server/zitadel/settings/v2beta/settings";

View File

@@ -3,7 +3,7 @@ import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import { import {
SettingsServiceClient, SettingsServiceClient,
SettingsServiceDefinition, SettingsServiceDefinition,
} from "../../proto/server/zitadel/settings/v2alpha/settings_service"; } from "../../proto/server/zitadel/settings/v2beta/settings_service";
import { ZitadelServer, createClient, getServers } from "../../server"; import { ZitadelServer, createClient, getServers } from "../../server";

View File

@@ -1,2 +1,2 @@
export * from "./user"; export * from "./user";
export * from "../../proto/server/zitadel/user/v2alpha/user"; export * from "../../proto/server/zitadel/user/v2beta/user";

View File

@@ -3,7 +3,7 @@ import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
import { import {
UserServiceClient, UserServiceClient,
UserServiceDefinition, UserServiceDefinition,
} from "../../proto/server/zitadel/user/v2alpha/user_service"; } from "../../proto/server/zitadel/user/v2beta/user_service";
import { ZitadelServer, createClient, getServers } from "../../server"; import { ZitadelServer, createClient, getServers } from "../../server";

569
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff