mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 06:52:24 +00:00
Merge pull request #40 from zitadel/oidc
feat: oidc proxy, handle authRequest, callback
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { Session } from "@zitadel/server";
|
||||
import { listSessions, server } from "#/lib/zitadel";
|
||||
import { getAllSessionIds } from "#/utils/cookies";
|
||||
import { getAllSessionCookieIds } from "#/utils/cookies";
|
||||
import { UserPlusIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import SessionsList from "#/ui/SessionsList";
|
||||
|
||||
async function loadSessions(): Promise<Session[]> {
|
||||
const ids = await getAllSessionIds();
|
||||
const ids = await getAllSessionCookieIds();
|
||||
|
||||
if (ids && ids.length) {
|
||||
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();
|
||||
|
||||
return (
|
||||
@@ -29,7 +35,7 @@ export default async function Page() {
|
||||
<p className="ztdl-p mb-6 block">Use your ZITADEL Account</p>
|
||||
|
||||
<div className="flex flex-col w-full space-y-2">
|
||||
<SessionsList sessions={sessions} />
|
||||
<SessionsList sessions={sessions} authRequestId={authRequestId} />
|
||||
<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="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">
|
||||
|
||||
122
apps/login/app/(login)/login/route.ts
Normal file
122
apps/login/app/(login)/login/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export default async function Page({
|
||||
searchParams: Record<string | number | symbol, string | undefined>;
|
||||
}) {
|
||||
const loginName = searchParams?.loginName;
|
||||
const authRequestId = searchParams?.authRequestId;
|
||||
const submit: boolean = searchParams?.submit === "true";
|
||||
|
||||
const loginSettings = await getLoginSettings(server);
|
||||
@@ -19,6 +20,7 @@ export default async function Page({
|
||||
<UsernameForm
|
||||
loginSettings={loginSettings}
|
||||
loginName={loginName}
|
||||
authRequestId={authRequestId}
|
||||
submit={submit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function Page({
|
||||
}: {
|
||||
searchParams: Record<string | number | symbol, string | undefined>;
|
||||
}) {
|
||||
const { loginName, altPassword } = searchParams;
|
||||
const { loginName, altPassword, authRequestId } = searchParams;
|
||||
|
||||
const sessionFactors = await loadSession(loginName);
|
||||
|
||||
@@ -48,6 +48,7 @@ export default async function Page({
|
||||
{loginName && (
|
||||
<LoginPasskey
|
||||
loginName={loginName}
|
||||
authRequestId={authRequestId}
|
||||
altPassword={altPassword === "true"}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async function Page({
|
||||
}: {
|
||||
searchParams: Record<string | number | symbol, string | undefined>;
|
||||
}) {
|
||||
const { loginName, promptPasswordless, alt } = searchParams;
|
||||
const { loginName, promptPasswordless, authRequestId, alt } = searchParams;
|
||||
const sessionFactors = await loadSession(loginName);
|
||||
|
||||
async function loadSession(loginName?: string) {
|
||||
@@ -46,6 +46,7 @@ export default async function Page({
|
||||
|
||||
<PasswordForm
|
||||
loginName={loginName}
|
||||
authRequestId={authRequestId}
|
||||
promptPasswordless={promptPasswordless === "true"}
|
||||
isAlternative={alt === "true"}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ProviderSlug } from "#/lib/demos";
|
||||
import { addHumanUser, server } from "#/lib/zitadel";
|
||||
import { server } from "#/lib/zitadel";
|
||||
import Alert, { AlertType } from "#/ui/Alert";
|
||||
import {
|
||||
AddHumanUserRequest,
|
||||
IDPInformation,
|
||||
RetrieveIdentityProviderInformationResponse,
|
||||
RetrieveIdentityProviderIntentResponse,
|
||||
user,
|
||||
IDPLink,
|
||||
} from "@zitadel/server";
|
||||
@@ -27,8 +27,8 @@ const PROVIDER_MAPPING: {
|
||||
// organisation: Organisation | undefined;
|
||||
profile: {
|
||||
displayName: idp.rawInformation?.User?.name ?? "",
|
||||
firstName: idp.rawInformation?.User?.given_name ?? "",
|
||||
lastName: idp.rawInformation?.User?.family_name ?? "",
|
||||
givenName: idp.rawInformation?.User?.given_name ?? "",
|
||||
familyName: idp.rawInformation?.User?.family_name ?? "",
|
||||
},
|
||||
idpLinks: [idpLink],
|
||||
};
|
||||
@@ -49,8 +49,8 @@ const PROVIDER_MAPPING: {
|
||||
// organisation: Organisation | undefined;
|
||||
profile: {
|
||||
displayName: idp.rawInformation?.name ?? "",
|
||||
firstName: idp.rawInformation?.name ?? "",
|
||||
lastName: idp.rawInformation?.name ?? "",
|
||||
givenName: idp.rawInformation?.name ?? "",
|
||||
familyName: idp.rawInformation?.name ?? "",
|
||||
},
|
||||
idpLinks: [idpLink],
|
||||
};
|
||||
@@ -64,8 +64,11 @@ function retrieveIDP(
|
||||
): Promise<IDPInformation | undefined> {
|
||||
const userService = user.getUser(server);
|
||||
return userService
|
||||
.retrieveIdentityProviderInformation({ intentId: id, token: token }, {})
|
||||
.then((resp: RetrieveIdentityProviderInformationResponse) => {
|
||||
.retrieveIdentityProviderIntent(
|
||||
{ idpIntentId: id, idpIntentToken: token },
|
||||
{}
|
||||
)
|
||||
.then((resp: RetrieveIdentityProviderIntentResponse) => {
|
||||
return resp.idpInformation;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { getSession, server } from "#/lib/zitadel";
|
||||
import { createCallback, getSession, server } from "#/lib/zitadel";
|
||||
import UserAvatar from "#/ui/UserAvatar";
|
||||
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}`);
|
||||
|
||||
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) => {
|
||||
if (response?.session) {
|
||||
return response.session;
|
||||
@@ -13,8 +22,8 @@ async function loadSession(loginName: string) {
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: { searchParams: any }) {
|
||||
const { loginName } = searchParams;
|
||||
const sessionFactors = await loadSession(loginName);
|
||||
const { loginName, authRequestId } = searchParams;
|
||||
const sessionFactors = await loadSession(loginName, authRequestId);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
|
||||
@@ -6,7 +6,13 @@ export async function POST(request: NextRequest) {
|
||||
if (body) {
|
||||
let { idpId, successUrl, failureUrl } = body;
|
||||
|
||||
return startIdentityProviderFlow(server, { idpId, successUrl, failureUrl })
|
||||
return startIdentityProviderFlow(server, {
|
||||
idpId,
|
||||
urls: {
|
||||
successUrl,
|
||||
failureUrl,
|
||||
},
|
||||
})
|
||||
.then((resp) => {
|
||||
return NextResponse.json(resp);
|
||||
})
|
||||
|
||||
@@ -1,53 +1,18 @@
|
||||
import {
|
||||
getSession,
|
||||
listAuthenticationMethodTypes,
|
||||
server,
|
||||
} from "#/lib/zitadel";
|
||||
import { getSessionCookieById } from "#/utils/cookies";
|
||||
import { listAuthenticationMethodTypes } from "#/lib/zitadel";
|
||||
import { createSessionAndUpdateCookie } from "#/utils/session";
|
||||
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) {
|
||||
const body = await request.json();
|
||||
if (body) {
|
||||
const { loginName } = body;
|
||||
const { loginName, authRequestId } = body;
|
||||
|
||||
const domain: string = request.nextUrl.hostname;
|
||||
|
||||
return createSessionAndUpdateCookie(loginName, undefined, domain, undefined)
|
||||
return createSessionAndUpdateCookie(
|
||||
loginName,
|
||||
undefined,
|
||||
undefined,
|
||||
authRequestId
|
||||
)
|
||||
.then((session) => {
|
||||
if (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 });
|
||||
});
|
||||
} else {
|
||||
throw "No user id found in session";
|
||||
throw { details: "No user id found in session" };
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
return NextResponse.json(
|
||||
{
|
||||
details: "could not add session to cookie",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json(error, { status: 500 });
|
||||
});
|
||||
} else {
|
||||
return NextResponse.error();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
createSessionAndUpdateCookie,
|
||||
setSessionAndUpdateCookie,
|
||||
} from "#/utils/session";
|
||||
import { RequestChallenges } from "@zitadel/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -17,12 +18,10 @@ export async function POST(request: NextRequest) {
|
||||
if (body) {
|
||||
const { loginName, password } = body;
|
||||
|
||||
const domain: string = request.nextUrl.hostname;
|
||||
|
||||
return createSessionAndUpdateCookie(
|
||||
loginName,
|
||||
password,
|
||||
domain,
|
||||
undefined,
|
||||
undefined
|
||||
).then((session) => {
|
||||
return NextResponse.json(session);
|
||||
@@ -44,7 +43,8 @@ export async function PUT(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
const { loginName, password, challenges, passkey } = body;
|
||||
const { loginName, password, webAuthN, authRequestId } = body;
|
||||
const challenges: RequestChallenges = body.challenges;
|
||||
|
||||
const recentPromise: Promise<SessionCookie> = loginName
|
||||
? getSessionCookieByLoginName(loginName).catch((error) => {
|
||||
@@ -56,16 +56,21 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
const domain: string = request.nextUrl.hostname;
|
||||
|
||||
if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) {
|
||||
challenges.webAuthN.domain = domain;
|
||||
}
|
||||
|
||||
return recentPromise
|
||||
.then((recent) => {
|
||||
console.log("setsession", webAuthN);
|
||||
return setSessionAndUpdateCookie(
|
||||
recent.id,
|
||||
recent.token,
|
||||
recent.loginName,
|
||||
password,
|
||||
passkey,
|
||||
domain,
|
||||
challenges
|
||||
webAuthN,
|
||||
challenges,
|
||||
authRequestId
|
||||
).then((session) => {
|
||||
return NextResponse.json({
|
||||
sessionId: session.id,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { stub } from "../support/mock";
|
||||
|
||||
describe("login", () => {
|
||||
beforeEach(() => {
|
||||
stub("zitadel.session.v2alpha.SessionService", "CreateSession", {
|
||||
stub("zitadel.session.v2beta.SessionService", "CreateSession", {
|
||||
data: {
|
||||
details: {
|
||||
sequence: 859,
|
||||
@@ -16,7 +16,7 @@ describe("login", () => {
|
||||
},
|
||||
});
|
||||
|
||||
stub("zitadel.session.v2alpha.SessionService", "GetSession", {
|
||||
stub("zitadel.session.v2beta.SessionService", "GetSession", {
|
||||
data: {
|
||||
session: {
|
||||
id: "221394658884845598",
|
||||
@@ -29,16 +29,15 @@ describe("login", () => {
|
||||
loginName: "john@zitadel.com",
|
||||
},
|
||||
password: undefined,
|
||||
passkey: undefined,
|
||||
webAuthN: undefined,
|
||||
intent: undefined,
|
||||
},
|
||||
metadata: {},
|
||||
domain: "localhost",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
stub("zitadel.settings.v2alpha.SettingsService", "GetLoginSettings", {
|
||||
stub("zitadel.settings.v2beta.SettingsService", "GetLoginSettings", {
|
||||
data: {
|
||||
settings: {
|
||||
passkeysType: 1,
|
||||
@@ -48,23 +47,19 @@ describe("login", () => {
|
||||
});
|
||||
describe("password login", () => {
|
||||
beforeEach(() => {
|
||||
stub(
|
||||
"zitadel.user.v2alpha.UserService",
|
||||
"ListAuthenticationMethodTypes",
|
||||
{
|
||||
data: {
|
||||
authMethodTypes: [1], // 1 for password authentication
|
||||
},
|
||||
}
|
||||
);
|
||||
stub("zitadel.user.v2beta.UserService", "ListAuthenticationMethodTypes", {
|
||||
data: {
|
||||
authMethodTypes: [1], // 1 for password authentication
|
||||
},
|
||||
});
|
||||
});
|
||||
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");
|
||||
});
|
||||
describe("with passkey prompt", () => {
|
||||
beforeEach(() => {
|
||||
stub("zitadel.session.v2alpha.SessionService", "SetSession", {
|
||||
stub("zitadel.session.v2beta.SessionService", "SetSession", {
|
||||
data: {
|
||||
details: {
|
||||
sequence: 859,
|
||||
@@ -91,18 +86,14 @@ describe("login", () => {
|
||||
});
|
||||
describe("passkey login", () => {
|
||||
beforeEach(() => {
|
||||
stub(
|
||||
"zitadel.user.v2alpha.UserService",
|
||||
"ListAuthenticationMethodTypes",
|
||||
{
|
||||
data: {
|
||||
authMethodTypes: [2], // 2 for passwordless authentication
|
||||
},
|
||||
}
|
||||
);
|
||||
stub("zitadel.user.v2beta.UserService", "ListAuthenticationMethodTypes", {
|
||||
data: {
|
||||
authMethodTypes: [2], // 2 for passwordless authentication
|
||||
},
|
||||
});
|
||||
});
|
||||
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(
|
||||
"eq",
|
||||
"/passkey/login"
|
||||
|
||||
@@ -4,7 +4,7 @@ const IDP_URL = "https://example.com/idp/url";
|
||||
|
||||
describe("register idps", () => {
|
||||
beforeEach(() => {
|
||||
stub("zitadel.user.v2alpha.UserService", "StartIdentityProviderFlow", {
|
||||
stub("zitadel.user.v2beta.UserService", "StartIdentityProviderIntent", {
|
||||
data: {
|
||||
authUrl: IDP_URL,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { stub } from "../support/mock";
|
||||
|
||||
describe("register", () => {
|
||||
beforeEach(() => {
|
||||
stub("zitadel.user.v2alpha.UserService", "AddHumanUser", {
|
||||
stub("zitadel.user.v2beta.UserService", "AddHumanUser", {
|
||||
data: {
|
||||
userId: "123",
|
||||
},
|
||||
|
||||
@@ -2,12 +2,12 @@ import { stub } from "../support/mock";
|
||||
|
||||
describe("/verify", () => {
|
||||
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.location("pathname", { timeout: 10_000 }).should("eq", "/loginname");
|
||||
});
|
||||
it("shows an error if validation failed", () => {
|
||||
stub("zitadel.user.v2alpha.UserService", "VerifyEmail", {
|
||||
stub("zitadel.user.v2beta.UserService", "VerifyEmail", {
|
||||
code: 3,
|
||||
error: "error validating code",
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ZitadelServer,
|
||||
ZitadelServerOptions,
|
||||
user,
|
||||
oidc,
|
||||
settings,
|
||||
getServers,
|
||||
initializeServer,
|
||||
@@ -19,16 +20,22 @@ import {
|
||||
GetSessionResponse,
|
||||
VerifyEmailResponse,
|
||||
SetSessionResponse,
|
||||
SetSessionRequest,
|
||||
DeleteSessionResponse,
|
||||
VerifyPasskeyRegistrationResponse,
|
||||
ChallengeKind,
|
||||
LoginSettings,
|
||||
GetLoginSettingsResponse,
|
||||
ListAuthenticationMethodTypesResponse,
|
||||
StartIdentityProviderFlowRequest,
|
||||
StartIdentityProviderFlowResponse,
|
||||
RetrieveIdentityProviderInformationRequest,
|
||||
RetrieveIdentityProviderInformationResponse,
|
||||
StartIdentityProviderIntentRequest,
|
||||
StartIdentityProviderIntentResponse,
|
||||
RetrieveIdentityProviderIntentRequest,
|
||||
RetrieveIdentityProviderIntentResponse,
|
||||
GetAuthRequestResponse,
|
||||
GetAuthRequestRequest,
|
||||
CreateCallbackRequest,
|
||||
CreateCallbackResponse,
|
||||
RequestChallenges,
|
||||
AddHumanUserRequest,
|
||||
} from "@zitadel/server";
|
||||
|
||||
export const zitadelConfig: ZitadelServerOptions = {
|
||||
@@ -95,9 +102,8 @@ export async function getPasswordComplexitySettings(
|
||||
export async function createSession(
|
||||
server: ZitadelServer,
|
||||
loginName: string,
|
||||
domain: string,
|
||||
password: string | undefined,
|
||||
challenges: ChallengeKind[] | undefined
|
||||
challenges: RequestChallenges | undefined
|
||||
): Promise<CreateSessionResponse | undefined> {
|
||||
const sessionService = session.getSession(server);
|
||||
return password
|
||||
@@ -105,12 +111,12 @@ export async function createSession(
|
||||
{
|
||||
checks: { user: { loginName }, password: { password } },
|
||||
challenges,
|
||||
domain,
|
||||
},
|
||||
{}
|
||||
)
|
||||
: sessionService.createSession(
|
||||
{ checks: { user: { loginName } }, domain },
|
||||
{ checks: { user: { loginName } }, challenges },
|
||||
|
||||
{}
|
||||
);
|
||||
}
|
||||
@@ -119,23 +125,29 @@ export async function setSession(
|
||||
server: ZitadelServer,
|
||||
sessionId: string,
|
||||
sessionToken: string,
|
||||
domain: string | undefined,
|
||||
password: string | undefined,
|
||||
passkey: { credentialAssertionData: any } | undefined,
|
||||
challenges: ChallengeKind[] | undefined
|
||||
webAuthN: { credentialAssertionData: any } | undefined,
|
||||
challenges: RequestChallenges | undefined
|
||||
): Promise<SetSessionResponse | undefined> {
|
||||
const sessionService = session.getSession(server);
|
||||
|
||||
const payload = { sessionId, sessionToken, challenges, domain };
|
||||
return password
|
||||
? sessionService.setSession(
|
||||
{
|
||||
...payload,
|
||||
checks: { password: { password }, passkey },
|
||||
},
|
||||
{}
|
||||
)
|
||||
: sessionService.setSession(payload, {});
|
||||
const payload: SetSessionRequest = {
|
||||
sessionId,
|
||||
sessionToken,
|
||||
challenges,
|
||||
checks: {},
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
if (password && payload.checks) {
|
||||
payload.checks.password = { password };
|
||||
}
|
||||
|
||||
if (webAuthN && payload.checks) {
|
||||
payload.checks.webAuthN = webAuthN;
|
||||
}
|
||||
|
||||
return sessionService.setSession(payload, {});
|
||||
}
|
||||
|
||||
export async function getSession(
|
||||
@@ -179,10 +191,10 @@ export async function addHumanUser(
|
||||
): Promise<string> {
|
||||
const userService = user.getUser(server);
|
||||
|
||||
const payload = {
|
||||
const payload: Partial<AddHumanUserRequest> = {
|
||||
email: { email },
|
||||
username: email,
|
||||
profile: { firstName, lastName },
|
||||
profile: { givenName: firstName, familyName: lastName },
|
||||
};
|
||||
return userService
|
||||
.addHumanUser(
|
||||
@@ -201,29 +213,48 @@ export async function addHumanUser(
|
||||
|
||||
export async function startIdentityProviderFlow(
|
||||
server: ZitadelServer,
|
||||
{ idpId, successUrl, failureUrl }: StartIdentityProviderFlowRequest
|
||||
): Promise<StartIdentityProviderFlowResponse> {
|
||||
{ idpId, urls }: StartIdentityProviderIntentRequest
|
||||
): Promise<StartIdentityProviderIntentResponse> {
|
||||
const userService = user.getUser(server);
|
||||
|
||||
return userService.startIdentityProviderFlow({
|
||||
return userService.startIdentityProviderIntent({
|
||||
idpId,
|
||||
successUrl,
|
||||
failureUrl,
|
||||
urls,
|
||||
});
|
||||
}
|
||||
|
||||
export async function retrieveIdentityProviderInformation(
|
||||
server: ZitadelServer,
|
||||
{ intentId, token }: RetrieveIdentityProviderInformationRequest
|
||||
): Promise<RetrieveIdentityProviderInformationResponse> {
|
||||
{ idpIntentId, idpIntentToken }: RetrieveIdentityProviderIntentRequest
|
||||
): Promise<RetrieveIdentityProviderIntentResponse> {
|
||||
const userService = user.getUser(server);
|
||||
|
||||
return userService.retrieveIdentityProviderInformation({
|
||||
intentId,
|
||||
token,
|
||||
return userService.retrieveIdentityProviderIntent({
|
||||
idpIntentId,
|
||||
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(
|
||||
server: ZitadelServer,
|
||||
userId: string,
|
||||
|
||||
28
apps/login/middleware.ts
Normal file
28
apps/login/middleware.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
[
|
||||
{
|
||||
"service": "zitadel.settings.v2alpha.SettingsService",
|
||||
"service": "zitadel.settings.v2beta.SettingsService",
|
||||
"method": "GetBrandingSettings",
|
||||
"out": {}
|
||||
},
|
||||
{
|
||||
"service": "zitadel.settings.v2alpha.SettingsService",
|
||||
"service": "zitadel.settings.v2beta.SettingsService",
|
||||
"method": "GetLegalAndSupportSettings",
|
||||
"out": {
|
||||
"data": {
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"service": "zitadel.settings.v2alpha.SettingsService",
|
||||
"service": "zitadel.settings.v2beta.SettingsService",
|
||||
"method": "GetActiveIdentityProviders",
|
||||
"out": {
|
||||
"data": {
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"service": "zitadel.settings.v2alpha.SettingsService",
|
||||
"service": "zitadel.settings.v2beta.SettingsService",
|
||||
"method": "GetPasswordComplexitySettings",
|
||||
"out": {
|
||||
"data": {
|
||||
@@ -1,6 +1,6 @@
|
||||
zitadel/user/v2alpha/user_service.proto
|
||||
zitadel/session/v2alpha/session_service.proto
|
||||
zitadel/settings/v2alpha/settings_service.proto
|
||||
zitadel/user/v2beta/user_service.proto
|
||||
zitadel/session/v2beta/session_service.proto
|
||||
zitadel/settings/v2beta/settings_service.proto
|
||||
zitadel/management.proto
|
||||
zitadel/auth.proto
|
||||
zitadel/admin.proto
|
||||
@@ -40,7 +40,6 @@
|
||||
"@zitadel/react": "workspace:*",
|
||||
"@zitadel/server": "workspace:*",
|
||||
"clsx": "1.2.1",
|
||||
"date-fns": "2.29.3",
|
||||
"moment": "^2.29.4",
|
||||
"next": "13.4.12",
|
||||
"next-themes": "^0.2.1",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Challenges_Passkey } from "@zitadel/server";
|
||||
import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64";
|
||||
import { Button, ButtonVariants } from "./Button";
|
||||
import Alert from "./Alert";
|
||||
@@ -10,10 +9,15 @@ import { Spinner } from "./Spinner";
|
||||
|
||||
type Props = {
|
||||
loginName: string;
|
||||
authRequestId?: string;
|
||||
altPassword: boolean;
|
||||
};
|
||||
|
||||
export default function LoginPasskey({ loginName, altPassword }: Props) {
|
||||
export default function LoginPasskey({
|
||||
loginName,
|
||||
authRequestId,
|
||||
altPassword,
|
||||
}: Props) {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
@@ -28,7 +32,7 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
|
||||
updateSessionForChallenge()
|
||||
.then((response) => {
|
||||
const pK =
|
||||
response.challenges.passkey.publicKeyCredentialRequestOptions
|
||||
response.challenges.webAuthN.publicKeyCredentialRequestOptions
|
||||
.publicKey;
|
||||
if (pK) {
|
||||
submitLoginAndContinue(pK)
|
||||
@@ -60,7 +64,13 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
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({
|
||||
loginName,
|
||||
passkey: data,
|
||||
webAuthN: { credentialAssertionData: data },
|
||||
authRequestId,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -115,18 +126,18 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
|
||||
})
|
||||
.then((assertedCredential: any) => {
|
||||
if (assertedCredential) {
|
||||
let authData = new Uint8Array(
|
||||
const authData = new Uint8Array(
|
||||
assertedCredential.response.authenticatorData
|
||||
);
|
||||
let clientDataJSON = new Uint8Array(
|
||||
const clientDataJSON = new Uint8Array(
|
||||
assertedCredential.response.clientDataJSON
|
||||
);
|
||||
let rawId = new Uint8Array(assertedCredential.rawId);
|
||||
let sig = new Uint8Array(assertedCredential.response.signature);
|
||||
let userHandle = new Uint8Array(
|
||||
const rawId = new Uint8Array(assertedCredential.rawId);
|
||||
const sig = new Uint8Array(assertedCredential.response.signature);
|
||||
const userHandle = new Uint8Array(
|
||||
assertedCredential.response.userHandle
|
||||
);
|
||||
let data = JSON.stringify({
|
||||
const data = {
|
||||
id: assertedCredential.id,
|
||||
rawId: coerceToBase64Url(rawId, "rawId"),
|
||||
type: assertedCredential.type,
|
||||
@@ -139,9 +150,21 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
|
||||
signature: coerceToBase64Url(sig, "sig"),
|
||||
userHandle: coerceToBase64Url(userHandle, "userHandle"),
|
||||
},
|
||||
});
|
||||
return submitLogin(data).then(() => {
|
||||
return router.push(`/accounts`);
|
||||
};
|
||||
return submitLogin(data).then((resp) => {
|
||||
return router.push(
|
||||
`/signedin?` +
|
||||
new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: resp.factors.user.loginName,
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: resp.factors.user.loginName,
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
@@ -169,11 +192,16 @@ export default function LoginPasskey({ loginName, altPassword }: Props) {
|
||||
<Button
|
||||
type="button"
|
||||
variant={ButtonVariants.Secondary}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
"/password?" + new URLSearchParams({ loginName, alt: "true" }) // alt is set because password is requested as alternative auth method, so passwordless prompt can be escaped
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const params = { loginName, alt: "true" };
|
||||
|
||||
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
|
||||
</Button>
|
||||
|
||||
@@ -14,12 +14,14 @@ type Inputs = {
|
||||
|
||||
type Props = {
|
||||
loginName?: string;
|
||||
authRequestId?: string;
|
||||
isAlternative?: boolean; // whether password was requested as alternative auth method
|
||||
promptPasswordless?: boolean;
|
||||
};
|
||||
|
||||
export default function PasswordForm({
|
||||
loginName,
|
||||
authRequestId,
|
||||
promptPasswordless,
|
||||
isAlternative,
|
||||
}: Props) {
|
||||
@@ -44,6 +46,7 @@ export default function PasswordForm({
|
||||
body: JSON.stringify({
|
||||
loginName,
|
||||
password: values.password,
|
||||
authRequestId,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -73,7 +76,19 @@ export default function PasswordForm({
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return router.push(`/accounts`);
|
||||
return router.push(
|
||||
`/signedin?` +
|
||||
new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: resp.factors.user.loginName,
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: resp.factors.user.loginName,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export default function RegisterFormWithoutPassword({ legal }: Props) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
loginName: loginName,
|
||||
// authRequestId, register does not need an oidc callback at the end
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import { XCircleIcon } from "@heroicons/react/24/outline";
|
||||
export default function SessionItem({
|
||||
session,
|
||||
reload,
|
||||
authRequestId,
|
||||
}: {
|
||||
session: Session;
|
||||
reload: () => void;
|
||||
authRequestId?: string;
|
||||
}) {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
@@ -39,7 +41,7 @@ export default function SessionItem({
|
||||
}
|
||||
|
||||
const validPassword = session?.factors?.password?.verifiedAt;
|
||||
const validPasskey = session?.factors?.passkey?.verifiedAt;
|
||||
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
|
||||
|
||||
const validUser = validPassword || validPasskey;
|
||||
|
||||
@@ -48,14 +50,29 @@ export default function SessionItem({
|
||||
href={
|
||||
validUser
|
||||
? `/signedin?` +
|
||||
new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
})
|
||||
new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
authRequestId,
|
||||
}
|
||||
: {
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
}
|
||||
)
|
||||
: `/loginname?` +
|
||||
new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
submit: "true",
|
||||
})
|
||||
new URLSearchParams(
|
||||
authRequestId
|
||||
? {
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -7,9 +7,10 @@ import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
sessions: Session[];
|
||||
authRequestId?: string;
|
||||
};
|
||||
|
||||
export default function SessionsList({ sessions }: Props) {
|
||||
export default function SessionsList({ sessions, authRequestId }: Props) {
|
||||
const [list, setList] = useState<Session[]>(sessions);
|
||||
return sessions ? (
|
||||
<div className="flex flex-col space-y-2">
|
||||
@@ -19,6 +20,7 @@ export default function SessionsList({ sessions }: Props) {
|
||||
return (
|
||||
<SessionItem
|
||||
session={session}
|
||||
authRequestId={authRequestId}
|
||||
reload={() => {
|
||||
setList(list.filter((s) => s.id !== session.id));
|
||||
}}
|
||||
|
||||
@@ -79,6 +79,7 @@ export default function SetPasswordForm({
|
||||
body: JSON.stringify({
|
||||
loginName: loginName,
|
||||
password: password,
|
||||
// authRequestId, register does not need an oidc callback
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface SignInWithIDPProps {
|
||||
}
|
||||
|
||||
const START_IDP_FLOW_PATH = (idpId: string) =>
|
||||
`/v2alpha/users/idps/${idpId}/start`;
|
||||
`/v2beta/users/idps/${idpId}/start`;
|
||||
|
||||
export function SignInWithIDP({
|
||||
host,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Spinner } from "./Spinner";
|
||||
import { LoginSettings } from "@zitadel/server";
|
||||
import Alert from "./Alert";
|
||||
|
||||
type Inputs = {
|
||||
loginName: string;
|
||||
@@ -15,12 +16,14 @@ type Inputs = {
|
||||
type Props = {
|
||||
loginSettings: LoginSettings | undefined;
|
||||
loginName: string | undefined;
|
||||
authRequestId: string | undefined;
|
||||
submit: boolean;
|
||||
};
|
||||
|
||||
export default function UsernameForm({
|
||||
loginSettings,
|
||||
loginName,
|
||||
authRequestId,
|
||||
submit,
|
||||
}: Props) {
|
||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||
@@ -37,19 +40,25 @@ export default function UsernameForm({
|
||||
|
||||
async function submitLoginName(values: Inputs) {
|
||||
setLoading(true);
|
||||
|
||||
const body = {
|
||||
loginName: values.loginName,
|
||||
};
|
||||
|
||||
const res = await fetch("/api/loginname", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
loginName: values.loginName,
|
||||
}),
|
||||
body: JSON.stringify(authRequestId ? { ...body, authRequestId } : body),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
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();
|
||||
}
|
||||
@@ -60,33 +69,40 @@ export default function UsernameForm({
|
||||
const method = response.authMethodTypes[0];
|
||||
switch (method) {
|
||||
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(
|
||||
"/password?" +
|
||||
new URLSearchParams(
|
||||
loginSettings?.passkeysType === 1
|
||||
? {
|
||||
loginName: values.loginName,
|
||||
promptPasswordless: `true`, // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
: { loginName: values.loginName }
|
||||
)
|
||||
"/password?" + new URLSearchParams(paramsPassword)
|
||||
);
|
||||
case 2: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||
const paramsPasskey: any = { loginName: values.loginName };
|
||||
if (authRequestId) {
|
||||
paramsPasskey.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" +
|
||||
new URLSearchParams({ loginName: values.loginName })
|
||||
"/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;
|
||||
}
|
||||
return router.push(
|
||||
"/password?" +
|
||||
new URLSearchParams(
|
||||
loginSettings?.passkeysType === 1
|
||||
? {
|
||||
loginName: values.loginName,
|
||||
promptPasswordless: `true`, // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
: { loginName: values.loginName }
|
||||
)
|
||||
"/password?" + new URLSearchParams(paramsPasskeyDefault)
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
@@ -99,12 +115,17 @@ export default function UsernameForm({
|
||||
} 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 (authRequestId) {
|
||||
passkeyParams.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" +
|
||||
new URLSearchParams({
|
||||
loginName: values.loginName,
|
||||
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
|
||||
})
|
||||
"/passkey/login?" + new URLSearchParams(passkeyParams)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,14 +149,16 @@ export default function UsernameForm({
|
||||
autoComplete="username"
|
||||
{...register("loginName", { required: "This field is required" })}
|
||||
label="Loginname"
|
||||
// error={errors.username?.message as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="py-4">
|
||||
<Alert>{error}</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex w-full flex-row items-center">
|
||||
{/* <Button type="button" variant={ButtonVariants.Secondary}>
|
||||
back
|
||||
</Button> */}
|
||||
<span className="flex-grow"></span>
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -7,6 +7,7 @@ export type SessionCookie = {
|
||||
token: string;
|
||||
loginName: string;
|
||||
changeDate: string;
|
||||
authRequestId?: string; // if its linked to an OIDC flow
|
||||
};
|
||||
|
||||
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 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
|
||||
* @param loginName
|
||||
|
||||
@@ -4,18 +4,17 @@ import {
|
||||
addSessionToCookie,
|
||||
updateSessionCookie,
|
||||
} from "./cookies";
|
||||
import { ChallengeKind, Session, Challenges } from "@zitadel/server";
|
||||
import { Session, Challenges, RequestChallenges } from "@zitadel/server";
|
||||
|
||||
export async function createSessionAndUpdateCookie(
|
||||
loginName: string,
|
||||
password: string | undefined,
|
||||
domain: string,
|
||||
challenges: ChallengeKind[] | undefined
|
||||
challenges: RequestChallenges | undefined,
|
||||
authRequestId: string | undefined
|
||||
): Promise<Session> {
|
||||
const createdSession = await createSession(
|
||||
server,
|
||||
loginName,
|
||||
domain,
|
||||
password,
|
||||
challenges
|
||||
);
|
||||
@@ -34,6 +33,10 @@ export async function createSessionAndUpdateCookie(
|
||||
loginName: response.session?.factors?.user?.loginName ?? "",
|
||||
};
|
||||
|
||||
if (authRequestId) {
|
||||
sessionCookie.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return addSessionToCookie(sessionCookie).then(() => {
|
||||
return response.session as Session;
|
||||
});
|
||||
@@ -55,17 +58,16 @@ export async function setSessionAndUpdateCookie(
|
||||
sessionToken: string,
|
||||
loginName: string,
|
||||
password: string | undefined,
|
||||
passkey: { credentialAssertionData: any } | undefined,
|
||||
domain: string | undefined,
|
||||
challenges: ChallengeKind[] | undefined
|
||||
webAuthN: { credentialAssertionData: any } | undefined,
|
||||
challenges: RequestChallenges | undefined,
|
||||
authRequestId: string | undefined
|
||||
): Promise<SessionWithChallenges> {
|
||||
return setSession(
|
||||
server,
|
||||
sessionId,
|
||||
sessionToken,
|
||||
domain,
|
||||
password,
|
||||
passkey,
|
||||
webAuthN,
|
||||
challenges
|
||||
).then((updatedSession) => {
|
||||
if (updatedSession) {
|
||||
@@ -76,24 +78,40 @@ export async function setSessionAndUpdateCookie(
|
||||
loginName: loginName,
|
||||
};
|
||||
|
||||
return getSession(server, sessionCookie.id, sessionCookie.token).then(
|
||||
(response) => {
|
||||
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 (authRequestId) {
|
||||
sessionCookie.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return updateSessionCookie(sessionCookie.id, newCookie).then(() => {
|
||||
return { challenges: updatedSession.challenges, ...session };
|
||||
});
|
||||
} else {
|
||||
throw "could not get session or session does not have loginName";
|
||||
return new Promise((resolve) => setTimeout(resolve, 1000)).then(() =>
|
||||
// TODO: remove
|
||||
getSession(server, sessionCookie.id, sessionCookie.token).then(
|
||||
(response) => {
|
||||
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 {
|
||||
throw "Session not be set";
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"test:unit:watch": "jest --watch",
|
||||
"dev": "tsup --watch --dts",
|
||||
"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": {
|
||||
"@bufbuild/buf": "^1.14.0",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"test:unit:watch": "jest --watch",
|
||||
"dev": "tsup --dts --watch",
|
||||
"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": {
|
||||
"@bufbuild/buf": "^1.14.0",
|
||||
|
||||
@@ -1,44 +1,58 @@
|
||||
import * as settings from "./v2/settings";
|
||||
import * as session from "./v2/session";
|
||||
import * as user from "./v2/user";
|
||||
import * as oidc from "./v2/oidc";
|
||||
import * as management from "./management";
|
||||
|
||||
import * as login from "./proto/server/zitadel/settings/v2alpha/login_settings";
|
||||
import * as password from "./proto/server/zitadel/settings/v2alpha/password_settings";
|
||||
import * as legal from "./proto/server/zitadel/settings/v2alpha/legal_settings";
|
||||
import * as login from "./proto/server/zitadel/settings/v2beta/login_settings";
|
||||
import * as password from "./proto/server/zitadel/settings/v2beta/password_settings";
|
||||
import * as legal from "./proto/server/zitadel/settings/v2beta/legal_settings";
|
||||
|
||||
export {
|
||||
BrandingSettings,
|
||||
Theme,
|
||||
} from "./proto/server/zitadel/settings/v2alpha/branding_settings";
|
||||
} from "./proto/server/zitadel/settings/v2beta/branding_settings";
|
||||
|
||||
export {
|
||||
LoginSettings,
|
||||
IdentityProvider,
|
||||
IdentityProviderType,
|
||||
} from "./proto/server/zitadel/settings/v2alpha/login_settings";
|
||||
} from "./proto/server/zitadel/settings/v2beta/login_settings";
|
||||
|
||||
export {
|
||||
ChallengeKind,
|
||||
RequestChallenges,
|
||||
Challenges,
|
||||
Challenges_Passkey,
|
||||
} from "./proto/server/zitadel/session/v2alpha/challenge";
|
||||
Challenges_WebAuthN,
|
||||
} 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 {
|
||||
Session,
|
||||
Factors,
|
||||
} from "./proto/server/zitadel/session/v2alpha/session";
|
||||
} from "./proto/server/zitadel/session/v2beta/session";
|
||||
export {
|
||||
IDPInformation,
|
||||
IDPLink,
|
||||
} from "./proto/server/zitadel/user/v2alpha/idp";
|
||||
} from "./proto/server/zitadel/user/v2beta/idp";
|
||||
export {
|
||||
ListSessionsResponse,
|
||||
GetSessionResponse,
|
||||
CreateSessionResponse,
|
||||
SetSessionResponse,
|
||||
SetSessionRequest,
|
||||
DeleteSessionResponse,
|
||||
} from "./proto/server/zitadel/session/v2alpha/session_service";
|
||||
} from "./proto/server/zitadel/session/v2beta/session_service";
|
||||
export {
|
||||
GetPasswordComplexitySettingsResponse,
|
||||
GetBrandingSettingsResponse,
|
||||
@@ -48,7 +62,7 @@ export {
|
||||
GetLoginSettingsRequest,
|
||||
GetActiveIdentityProvidersResponse,
|
||||
GetActiveIdentityProvidersRequest,
|
||||
} from "./proto/server/zitadel/settings/v2alpha/settings_service";
|
||||
} from "./proto/server/zitadel/settings/v2beta/settings_service";
|
||||
export {
|
||||
AddHumanUserResponse,
|
||||
AddHumanUserRequest,
|
||||
@@ -62,19 +76,19 @@ export {
|
||||
ListAuthenticationMethodTypesResponse,
|
||||
ListAuthenticationMethodTypesRequest,
|
||||
AuthenticationMethodType,
|
||||
StartIdentityProviderFlowRequest,
|
||||
StartIdentityProviderFlowResponse,
|
||||
RetrieveIdentityProviderInformationRequest,
|
||||
RetrieveIdentityProviderInformationResponse,
|
||||
} from "./proto/server/zitadel/user/v2alpha/user_service";
|
||||
StartIdentityProviderIntentRequest,
|
||||
StartIdentityProviderIntentResponse,
|
||||
RetrieveIdentityProviderIntentRequest,
|
||||
RetrieveIdentityProviderIntentResponse,
|
||||
} from "./proto/server/zitadel/user/v2beta/user_service";
|
||||
export {
|
||||
SetHumanPasswordResponse,
|
||||
SetHumanPasswordRequest,
|
||||
} from "./proto/server/zitadel/management";
|
||||
export * from "./proto/server/zitadel/idp";
|
||||
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings";
|
||||
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2alpha/password_settings";
|
||||
export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2alpha/settings";
|
||||
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2beta/legal_settings";
|
||||
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2beta/password_settings";
|
||||
export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2beta/settings";
|
||||
|
||||
import {
|
||||
getServers,
|
||||
@@ -96,4 +110,5 @@ export {
|
||||
login,
|
||||
password,
|
||||
legal,
|
||||
oidc,
|
||||
};
|
||||
|
||||
2
packages/zitadel-server/src/v2/oidc/index.ts
Normal file
2
packages/zitadel-server/src/v2/oidc/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./oidc";
|
||||
export * from "../../proto/server/zitadel/oidc/v2beta/oidc_service";
|
||||
24
packages/zitadel-server/src/v2/oidc/oidc.ts
Normal file
24
packages/zitadel-server/src/v2/oidc/oidc.ts
Normal 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
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./session";
|
||||
export * from "../../proto/server/zitadel/session/v2alpha/session";
|
||||
export * from "../../proto/server/zitadel/session/v2beta/session";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
|
||||
import {
|
||||
SessionServiceClient,
|
||||
SessionServiceDefinition,
|
||||
} from "../../proto/server/zitadel/session/v2alpha/session_service";
|
||||
} from "../../proto/server/zitadel/session/v2beta/session_service";
|
||||
|
||||
import { ZitadelServer, createClient, getServers } from "../../server";
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./settings";
|
||||
export * from "../../proto/server/zitadel/settings/v2alpha/settings";
|
||||
export * from "../../proto/server/zitadel/settings/v2beta/settings";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
|
||||
import {
|
||||
SettingsServiceClient,
|
||||
SettingsServiceDefinition,
|
||||
} from "../../proto/server/zitadel/settings/v2alpha/settings_service";
|
||||
} from "../../proto/server/zitadel/settings/v2beta/settings_service";
|
||||
|
||||
import { ZitadelServer, createClient, getServers } from "../../server";
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./user";
|
||||
export * from "../../proto/server/zitadel/user/v2alpha/user";
|
||||
export * from "../../proto/server/zitadel/user/v2beta/user";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CompatServiceDefinition } from "nice-grpc/lib/service-definitions";
|
||||
import {
|
||||
UserServiceClient,
|
||||
UserServiceDefinition,
|
||||
} from "../../proto/server/zitadel/user/v2alpha/user_service";
|
||||
} from "../../proto/server/zitadel/user/v2beta/user_service";
|
||||
|
||||
import { ZitadelServer, createClient, getServers } from "../../server";
|
||||
|
||||
|
||||
569
pnpm-lock.yaml
generated
569
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user