diff --git a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx index 6ae205bcb5f..2be2c54b019 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -1,13 +1,22 @@ -import { ProviderSlug } from "@/lib/demos"; -import { getBrandingSettings, PROVIDER_NAME_MAPPING } from "@/lib/zitadel"; +import { getBrandingSettings } from "@/lib/zitadel"; import DynamicTheme from "@/ui/DynamicTheme"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; + +// This configuration shows the given name in the respective IDP button as fallback +const PROVIDER_NAME_MAPPING: { + [provider: string]: string; +} = { + [IdentityProviderType.GOOGLE]: "Google", + [IdentityProviderType.GITHUB]: "GitHub", + [IdentityProviderType.AZURE_AD]: "Microsoft", +}; export default async function Page({ searchParams, params, }: { searchParams: Record; - params: { provider: ProviderSlug }; + params: { provider: string }; }) { const { id, token, authRequestId, organization } = searchParams; const { provider } = params; diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index ee6148818fc..c144186bf6e 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,174 +1,96 @@ -import { ProviderSlug } from "@/lib/demos"; -import { getBrandingSettings, userService } from "@/lib/zitadel"; +import { PROVIDER_MAPPING } from "@/lib/idp"; +import { + addIDPLink, + createUser, + getBrandingSettings, + getIDPByID, + listUsers, + retrieveIDPIntent, +} from "@/lib/zitadel"; import Alert, { AlertType } from "@/ui/Alert"; import DynamicTheme from "@/ui/DynamicTheme"; import IdpSignin from "@/ui/IdpSignin"; -import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { IDPInformation, IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb"; -import { PartialMessage } from "@zitadel/client"; - -const PROVIDER_MAPPING: { - [provider: string]: ( - rI: IDPInformation, - ) => PartialMessage; -} = { - [ProviderSlug.GOOGLE]: (idp: IDPInformation) => { - const rawInfo = idp.rawInformation?.toJson() as { - User: { - email: string; - name?: string; - given_name?: string; - family_name?: string; - }; - }; - - const idpLink: PartialMessage = { - idpId: idp.idpId, - userId: idp.userId, - userName: idp.userName, - }; - - const req: PartialMessage = { - username: idp.userName, - email: { - email: rawInfo.User?.email, - verification: { case: "isVerified", value: true }, - }, - // organisation: Organisation | undefined; - profile: { - displayName: rawInfo.User?.name ?? "", - givenName: rawInfo.User?.given_name ?? "", - familyName: rawInfo.User?.family_name ?? "", - }, - idpLinks: [idpLink], - }; - return req; - }, - [ProviderSlug.AZURE]: (idp: IDPInformation) => { - const rawInfo = idp.rawInformation?.toJson() as { - mail: string; - displayName?: string; - givenName?: string; - surname?: string; - }; - - const idpLink: PartialMessage = { - idpId: idp.idpId, - userId: idp.userId, - userName: idp.userName, - }; - - const req: PartialMessage = { - username: idp.userName, - email: { - email: rawInfo?.mail, - verification: { case: "isVerified", value: true }, - }, - // organisation: Organisation | undefined; - profile: { - displayName: rawInfo?.displayName ?? "", - givenName: rawInfo?.givenName ?? "", - familyName: rawInfo?.surname ?? "", - }, - idpLinks: [idpLink], - }; - return req; - }, - [ProviderSlug.GITHUB]: (idp: IDPInformation) => { - const rawInfo = idp.rawInformation?.toJson() as { - email: string; - name: string; - }; - const idpLink: PartialMessage = { - idpId: idp.idpId, - userId: idp.userId, - userName: idp.userName, - }; - const req: PartialMessage = { - username: idp.userName, - email: { - email: rawInfo?.email, - verification: { case: "isVerified", value: true }, - }, - // organisation: Organisation | undefined; - profile: { - displayName: rawInfo?.name ?? "", - givenName: rawInfo?.name ?? "", - familyName: rawInfo?.name ?? "", - }, - idpLinks: [idpLink], - }; - return req; - }, -}; - -function retrieveIDPIntent(id: string, token: string) { - return userService.retrieveIdentityProviderIntent( - { idpIntentId: id, idpIntentToken: token }, - {}, - ); -} - -function createUser( - provider: ProviderSlug, - info: IDPInformation, -): Promise { - const userData = PROVIDER_MAPPING[provider](info); - return userService.addHumanUser(userData, {}).then((resp) => resp.userId); -} +import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; export default async function Page({ searchParams, params, }: { searchParams: Record; - params: { provider: ProviderSlug }; + params: { provider: string }; }) { const { id, token, authRequestId, organization } = searchParams; const { provider } = params; const branding = await getBrandingSettings(organization); - if (provider && id && token) { return retrieveIDPIntent(id, token) - .then((resp) => { + .then(async (resp) => { const { idpInformation, userId } = resp; - if (idpInformation) { - // handle login - if (userId) { - return ( - -
-

Login successful

-
You have successfully been loggedIn!
+ if (userId) { + // TODO: update user if idp.options.isAutoUpdate is true - -
-
- ); - } else { - // handle register - return createUser(provider, idpInformation) - .then((userId) => { + return ( + +
+

Login successful

+
You have successfully been loggedIn!
+ + +
+
+ ); + } + + if (idpInformation) { + const idp = await getIDPByID(idpInformation.idpId); + const options = idp?.config?.options; + + // search for potential user via username, then link + if (options?.isLinkingAllowed) { + let foundUser; + const email = + PROVIDER_MAPPING[provider](idpInformation).email?.email; + + if (options.autoLinking === AutoLinkingOption.EMAIL && email) { + foundUser = await listUsers({ email }).then((response) => { + return response.result ? response.result[0] : null; + }); + } else if (options.autoLinking === AutoLinkingOption.USERNAME) { + foundUser = await listUsers( + options.autoLinking === AutoLinkingOption.USERNAME + ? { userName: idpInformation.userName } + : { email }, + ).then((response) => { + return response.result ? response.result[0] : null; + }); + } else { + foundUser = await listUsers({ + userName: idpInformation.userName, + email, + }).then((response) => { + return response.result ? response.result[0] : null; + }); + } + + if (foundUser) { + const idpLink = await addIDPLink( + { + id: idpInformation.idpId, + userId: idpInformation.userId, + userName: idpInformation.userName, + }, + foundUser.userId, + ).catch((error) => { return (
-

Register successful

-
You have successfully been registered!
-
-
- ); - }) - .catch((error) => { - return ( - -
-

Register failed

+

Linking failed

{ @@ -180,9 +102,66 @@ export default async function Page({ ); }); + + if (idpLink) { + return ( + // TODO: possibily login user now + +
+

Account successfully linked

+
Your account has successfully been linked!
+
+
+ ); + } + } } + + if (options?.isCreationAllowed && options.isAutoCreation) { + const newUser = await createUser(provider, idpInformation); + + if (newUser) { + return ( + +
+

Register successful

+
You have successfully been registered!
+
+
+ ); + } + } + + // return login failed if no linking or creation is allowed and no user was found + return ( + +
+

Login failed

+
+ { + + User could not be logged in + + } +
+
+
+ ); } else { - throw new Error("Could not get user information."); + return ( + +
+

Login failed

+
+ { + + Could not get user information + + } +
+
+
+ ); } }) .catch((error) => { diff --git a/apps/login/src/app/api/loginname/route.ts b/apps/login/src/app/api/loginname/route.ts index ccc8a77bbd5..b6751ae9164 100644 --- a/apps/login/src/app/api/loginname/route.ts +++ b/apps/login/src/app/api/loginname/route.ts @@ -1,4 +1,4 @@ -import { ProviderSlug } from "@/lib/demos"; +import { idpTypeToSlug } from "@/lib/idp"; import { getActiveIdentityProviders, getLoginSettings, @@ -7,14 +7,16 @@ import { startIdentityProviderFlow, } from "@/lib/zitadel"; import { createSessionForUserIdAndUpdateCookie } from "@/utils/session"; -import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { const body = await request.json(); if (body) { const { loginName, authRequestId, organization } = body; - return listUsers(loginName, organization).then(async (users) => { + return listUsers({ + userName: loginName, + organizationId: organization, + }).then(async (users) => { if (users.details?.totalResult == BigInt(1) && users.result[0].userId) { const userId = users.result[0].userId; return createSessionForUserIdAndUpdateCookie( @@ -64,28 +66,8 @@ export async function POST(request: NextRequest) { const host = request.nextUrl.origin; const identityProviderType = identityProviders[0].type; - let provider: string; - switch (identityProviderType) { - case IdentityProviderType.GITHUB: - provider = "github"; - break; - case IdentityProviderType.GOOGLE: - provider = "google"; - break; - case IdentityProviderType.AZURE_AD: - provider = "azure"; - break; - case IdentityProviderType.SAML: - provider = "saml"; - break; - case IdentityProviderType.OIDC: - provider = "oidc"; - break; - default: - provider = "oidc"; - break; - } + const provider = idpTypeToSlug(identityProviderType); const params = new URLSearchParams(); diff --git a/apps/login/src/app/api/passkeys/verify/route.ts b/apps/login/src/app/api/passkeys/verify/route.ts index 064ba6e77a3..ea25a9ba5f0 100644 --- a/apps/login/src/app/api/passkeys/verify/route.ts +++ b/apps/login/src/app/api/passkeys/verify/route.ts @@ -18,14 +18,19 @@ export async function POST(request: NextRequest) { const session = await getSession(sessionCookie.id, sessionCookie.token); const userId = session?.session?.factors?.user?.id; - + console.log("payload", { + passkeyId, + passkeyName, + publicKeyCredential, + userId, + }); if (userId) { - return verifyPasskeyRegistration( + return verifyPasskeyRegistration({ passkeyId, passkeyName, publicKeyCredential, userId, - ) + }) .then((resp) => { return NextResponse.json(resp); }) diff --git a/apps/login/src/app/api/resetpassword/route.ts b/apps/login/src/app/api/resetpassword/route.ts index 570eb649925..8ea375adcee 100644 --- a/apps/login/src/app/api/resetpassword/route.ts +++ b/apps/login/src/app/api/resetpassword/route.ts @@ -5,7 +5,10 @@ export async function POST(request: NextRequest) { const body = await request.json(); if (body) { const { loginName, organization } = body; - return listUsers(loginName, organization).then((users) => { + return listUsers({ + userName: loginName, + organizationId: organization, + }).then((users) => { if ( users.details && Number(users.details.totalResult) == 1 && diff --git a/apps/login/src/app/api/u2f/verify/route.ts b/apps/login/src/app/api/u2f/verify/route.ts index a842c8065c8..87f0a94d158 100644 --- a/apps/login/src/app/api/u2f/verify/route.ts +++ b/apps/login/src/app/api/u2f/verify/route.ts @@ -22,12 +22,15 @@ export async function POST(request: NextRequest) { const userId = session?.session?.factors?.user?.id; if (userId) { - const req: PlainMessage = { + let req: PlainMessage = { publicKeyCredential, u2fId, userId, tokenName: passkeyName, }; + + req = VerifyU2FRegistrationRequest.fromJson(request as any); + return verifyU2FRegistration(req) .then((resp) => { return NextResponse.json(resp); diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 80b3bed72a8..85eae70ee0c 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -18,6 +18,7 @@ import { Prompt, } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { idpTypeToSlug } from "@/lib/idp"; async function loadSessions(ids: string[]): Promise { const response = await listSessions( @@ -168,28 +169,7 @@ export async function GET(request: NextRequest) { const host = request.nextUrl.origin; const identityProviderType = identityProviders[0].type; - let provider: string; - - switch (identityProviderType) { - case IdentityProviderType.GITHUB: - provider = "github"; - break; - case IdentityProviderType.GOOGLE: - provider = "google"; - break; - case IdentityProviderType.AZURE_AD: - provider = "azure"; - break; - case IdentityProviderType.SAML: - provider = "saml"; - break; - case IdentityProviderType.OIDC: - provider = "oidc"; - break; - default: - provider = "oidc"; - break; - } + let provider = idpTypeToSlug(identityProviderType); const params = new URLSearchParams(); diff --git a/apps/login/src/lib/demos.ts b/apps/login/src/lib/demos.ts index 13e2a3017a8..38912e50e5a 100644 --- a/apps/login/src/lib/demos.ts +++ b/apps/login/src/lib/demos.ts @@ -4,12 +4,6 @@ export type Item = { description?: string; }; -export enum ProviderSlug { - GOOGLE = "google", - GITHUB = "github", - AZURE = "microsoft", -} - export const demos: { name: string; items: Item[] }[] = [ { name: "Login", diff --git a/apps/login/src/lib/idp.ts b/apps/login/src/lib/idp.ts new file mode 100644 index 00000000000..3b0ee29f9d0 --- /dev/null +++ b/apps/login/src/lib/idp.ts @@ -0,0 +1,131 @@ +import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { IDPInformation, IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { PartialMessage } from "@zitadel/client"; + +// This maps the IdentityProviderType to a slug which is used in the /success and /failure routes +export function idpTypeToSlug(idpType: IdentityProviderType) { + switch (idpType) { + case IdentityProviderType.GITHUB: + return "github"; + case IdentityProviderType.GOOGLE: + return "google"; + case IdentityProviderType.AZURE_AD: + return "azure"; + case IdentityProviderType.SAML: + return "saml"; + case IdentityProviderType.OIDC: + return "oidc"; + default: + throw new Error("Unknown identity provider type"); + } +} + +// this maps the IDPInformation to the AddHumanUserRequest which is used when creating a user or linking a user (email) +// TODO: extend this object from a other file which can be overwritten by customers like map = { ...PROVIDER_MAPPING, ...customerMap } +export type OIDC_USER = { + User: { + email: string; + name?: string; + given_name?: string; + family_name?: string; + }; +}; + +export const PROVIDER_MAPPING: { + [provider: string]: ( + rI: IDPInformation, + ) => PartialMessage; +} = { + [idpTypeToSlug(IdentityProviderType.GOOGLE)]: (idp: IDPInformation) => { + const rawInfo = idp.rawInformation?.toJson() as OIDC_USER; + console.log(rawInfo); + const idpLink: PartialMessage = { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }; + + const req: PartialMessage = { + username: idp.userName, + email: { + email: rawInfo.User?.email, + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.User?.name ?? "", + givenName: rawInfo.User?.given_name ?? "", + familyName: rawInfo.User?.family_name ?? "", + }, + idpLinks: [idpLink], + }; + + return req; + }, + [idpTypeToSlug(IdentityProviderType.AZURE_AD)]: (idp: IDPInformation) => { + const rawInfo = idp.rawInformation?.toJson() as { + jobTitle: string; + mail: string; + mobilePhone: string; + preferredLanguage: string; + id: string; + displayName?: string; + givenName?: string; + surname?: string; + officeLocation?: string; + userPrincipalName: string; + }; + + const idpLink: PartialMessage = { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }; + + console.log(rawInfo, rawInfo.userPrincipalName); + + const req: PartialMessage = { + username: idp.userName, + email: { + email: rawInfo.mail || rawInfo.userPrincipalName || "", + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.displayName ?? "", + givenName: rawInfo.givenName ?? "", + familyName: rawInfo.surname ?? "", + }, + idpLinks: [idpLink], + }; + + return req; + }, + [idpTypeToSlug(IdentityProviderType.GITHUB)]: (idp: IDPInformation) => { + const rawInfo = idp.rawInformation?.toJson() as { + email: string; + name: string; + }; + + const idpLink: PartialMessage = { + idpId: idp.idpId, + userId: idp.userId, + userName: idp.userName, + }; + + const req: PartialMessage = { + username: idp.userName, + email: { + email: rawInfo.email, + verification: { case: "isVerified", value: true }, + }, + profile: { + displayName: rawInfo.name ?? "", + givenName: rawInfo.name ?? "", + familyName: rawInfo.name ?? "", + }, + idpLinks: [idpLink], + }; + + return req; + }, +}; diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 2e26fada2be..f075cc88a2f 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -3,23 +3,27 @@ import { createSessionServiceClient, createSettingsServiceClient, createUserServiceClient, + createIdpServiceClient, makeReqCtx, } from "@zitadel/client/v2"; import { createManagementServiceClient } from "@zitadel/client/v1"; import { createServerTransport } from "@zitadel/node"; -import { GetActiveIdentityProvidersRequest } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { RetrieveIdentityProviderIntentRequest, + VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import type { RedirectURLs } from "@zitadel/proto/zitadel/user/v2/idp_pb"; -import { ProviderSlug } from "./demos"; -import { PlainMessage } from "@zitadel/client"; +import { PartialMessage, PlainMessage } from "@zitadel/client"; +import { SearchQuery as UserSearchQuery } from "@zitadel/proto/zitadel/user/v2/query_pb"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { PROVIDER_MAPPING } from "./idp"; const SESSION_LIFETIME_S = 3000; @@ -35,6 +39,8 @@ export const sessionService = createSessionServiceClient(transport); export const managementService = createManagementServiceClient(transport); export const userService = createUserServiceClient(transport); export const oidcService = createOIDCServiceClient(transport); +export const idpService = createIdpServiceClient(transport); + export const settingsService = createSettingsServiceClient(transport); export async function getBrandingSettings(organization?: string) { @@ -233,40 +239,54 @@ export async function getUserByID(userId: string) { return userService.getUserByID({ userId }, {}); } -export async function listUsers(userName: string, organizationId: string) { +export async function listUsers({ + userName, + email, + organizationId, +}: { + userName?: string; + email?: string; + organizationId?: string; +}) { + const queries: PartialMessage[] = []; + + if (userName) { + queries.push({ + query: { + case: "userNameQuery", + value: { + userName, + method: TextQueryMethod.EQUALS, + }, + }, + }); + } + + if (organizationId) { + queries.push({ + query: { + case: "organizationIdQuery", + value: { + organizationId, + }, + }, + }); + } + + if (email) { + queries.push({ + query: { + case: "emailQuery", + value: { + emailAddress: email, + }, + }, + }); + } + return userService.listUsers( { - queries: organizationId - ? [ - { - query: { - case: "userNameQuery", - value: { - userName, - method: TextQueryMethod.EQUALS, - }, - }, - }, - { - query: { - case: "organizationIdQuery", - value: { - organizationId, - }, - }, - }, - ] - : [ - { - query: { - case: "userNameQuery", - value: { - userName, - method: TextQueryMethod.EQUALS, - }, - }, - }, - ], + queries: queries, }, {}, ); @@ -276,14 +296,6 @@ export async function getOrgByDomain(domain: string) { return managementService.getOrgByDomainGlobal({ domain }, {}); } -export const PROVIDER_NAME_MAPPING: { - [provider: string]: string; -} = { - [ProviderSlug.GOOGLE]: "Google", - [ProviderSlug.GITHUB]: "GitHub", - [ProviderSlug.AZURE]: "Microft", -}; - export async function startIdentityProviderFlow({ idpId, urls, @@ -348,6 +360,44 @@ export async function resendEmailCode(userId: string) { ); } +export function retrieveIDPIntent(id: string, token: string) { + return userService.retrieveIdentityProviderIntent( + { idpIntentId: id, idpIntentToken: token }, + {}, + ); +} + +export function getIDPByID(id: string) { + return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp); +} + +export function addIDPLink( + idp: { + id: string; + userId: string; + userName: string; + }, + userId: string, +) { + return userService.addIDPLink( + { + idpLink: { + userId: idp.userId, + idpId: idp.id, + userName: idp.userName, + }, + userId, + }, + {}, + ); +} + +export function createUser(provider: string, info: IDPInformation) { + const userData = PROVIDER_MAPPING[provider](info); + console.log("ud", userData); + return userService.addHumanUser(userData, {}); +} + /** * * @param userId the id of the user where the email should be set @@ -433,24 +483,11 @@ export async function getActiveIdentityProviders(orgId?: string) { * @returns the newly set email */ export async function verifyPasskeyRegistration( - passkeyId: string, - passkeyName: string, - publicKeyCredential: - | { - [key: string]: any; - } - | undefined, - userId: string, + request: PartialMessage, ) { - return userService.verifyPasskeyRegistration( - { - passkeyId, - passkeyName, - publicKeyCredential, - userId, - }, - {}, - ); + // TODO: find a better way to handle this + request = VerifyPasskeyRegistrationRequest.fromJson(request as any); + return userService.verifyPasskeyRegistration(request, {}); } /** diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 982d5f5b150..965150efbcd 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -18,11 +18,14 @@ export function middleware(request: NextRequest) { requestHeaders.set("x-zitadel-login-client", SERVICE_USER_ID); // this is a workaround for the next.js server not forwarding the host header - requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); - // requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); + // requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); + requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); // this is a workaround for the next.js server not forwarding the host header - // requestHeaders.set("x-zitadel-instance-host", `${INSTANCE}`); + requestHeaders.set( + "x-zitadel-instance-host", + `${INSTANCE}`.replace("https://", ""), + ); const responseHeaders = new Headers(); responseHeaders.set("Access-Control-Allow-Origin", "*"); diff --git a/apps/login/src/ui/LoginPasskey.tsx b/apps/login/src/ui/LoginPasskey.tsx index fce8ecd8ecb..c29d9846d97 100644 --- a/apps/login/src/ui/LoginPasskey.tsx +++ b/apps/login/src/ui/LoginPasskey.tsx @@ -8,6 +8,7 @@ import Alert from "./Alert"; import { Spinner } from "./Spinner"; import BackButton from "./BackButton"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; // either loginName or sessionId must be provided type Props = { @@ -78,7 +79,7 @@ export default function LoginPasskey({ loginName, sessionId, organization, - challenges: { + challenges: RequestChallenges.fromJson({ webAuthN: { domain: "", // USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0; @@ -87,7 +88,7 @@ export default function LoginPasskey({ // USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa userVerificationRequirement: userVerificationRequirement, }, - }, + }), authRequestId, }), }); diff --git a/apps/login/src/ui/RegisterPasskey.tsx b/apps/login/src/ui/RegisterPasskey.tsx index 4ad72b583ba..6b2bdb1fb66 100644 --- a/apps/login/src/ui/RegisterPasskey.tsx +++ b/apps/login/src/ui/RegisterPasskey.tsx @@ -92,7 +92,7 @@ export default function RegisterPasskey({ return submitRegister().then((resp: RegisterPasskeyResponse) => { const passkeyId = resp.passkeyId; const options: CredentialCreationOptions = - (resp.publicKeyCredentialCreationOptions?.toJson() as CredentialCreationOptions) ?? + (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? {}; if (options?.publicKey) { @@ -143,6 +143,7 @@ export default function RegisterPasskey({ ), }, }; + return submitVerify(passkeyId, "", data, sessionId).then(() => { const params = new URLSearchParams(); @@ -194,19 +195,32 @@ export default function RegisterPasskey({ type="button" variant={ButtonVariants.Secondary} onClick={() => { - const params = new URLSearchParams(); if (authRequestId) { - params.set("authRequest", authRequestId); - } - if (sessionId) { - params.set("sessionId", sessionId); - } + const params = new URLSearchParams({ + authRequest: authRequestId, + }); - if (organization) { - params.set("organization", organization); - } + if (sessionId) { + params.set("sessionId", sessionId); + } - router.push("/login?" + params); + if (organization) { + params.set("organization", organization); + } + + router.push("/login?" + params); + } else { + const params = new URLSearchParams(); + + if (sessionId) { + params.append("sessionId", sessionId); + } + if (organization) { + params.append("organization", organization); + } + + router.push("/signedin?" + params); + } }} > skip diff --git a/apps/login/src/ui/RegisterU2F.tsx b/apps/login/src/ui/RegisterU2F.tsx index eebbbe87b29..01081cd5eed 100644 --- a/apps/login/src/ui/RegisterU2F.tsx +++ b/apps/login/src/ui/RegisterU2F.tsx @@ -90,7 +90,7 @@ export default function RegisterU2F({ return submitRegister().then((resp: RegisterU2FResponse) => { const u2fId = resp.u2fId; const options: CredentialCreationOptions = - (resp.publicKeyCredentialCreationOptions?.toJson() as CredentialCreationOptions) ?? + (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? {}; if (options.publicKey) { diff --git a/apps/login/src/ui/SessionItem.tsx b/apps/login/src/ui/SessionItem.tsx index 8f68916d099..f036fd01392 100644 --- a/apps/login/src/ui/SessionItem.tsx +++ b/apps/login/src/ui/SessionItem.tsx @@ -121,11 +121,12 @@ export default function SessionItem({ + onClick={(event) => { + event.preventDefault(); clearSession(session.id).then(() => { reload(); - }) - } + }); + }} />
diff --git a/apps/login/src/ui/SignInWithIDP.tsx b/apps/login/src/ui/SignInWithIDP.tsx index dce657b9045..af82aafc5bf 100644 --- a/apps/login/src/ui/SignInWithIDP.tsx +++ b/apps/login/src/ui/SignInWithIDP.tsx @@ -1,6 +1,6 @@ "use client"; -import { ReactNode, useState } from "react"; +import { ReactNode, useState } from "react"; import { SignInWithGitlab, SignInWithAzureAD, @@ -8,10 +8,10 @@ import { SignInWithGithub, } from "@zitadel/react"; import { useRouter } from "next/navigation"; -import { ProviderSlug } from "@/lib/demos"; import Alert from "./Alert"; -import BackButton from "./BackButton"; import { IdentityProvider } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { idpTypeToSlug } from "@/lib/idp"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; export interface SignInWithIDPProps { children?: ReactNode; @@ -36,7 +36,7 @@ export function SignInWithIDP({ const [error, setError] = useState(""); const router = useRouter(); - async function startFlow(idpId: string, provider: ProviderSlug) { + async function startFlow(idpId: string, provider: string) { setLoading(true); const params = new URLSearchParams(); @@ -78,62 +78,65 @@ export function SignInWithIDP({ {identityProviders && identityProviders.map((idp, i) => { switch (idp.type) { - case 6: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITHUB: + case IdentityProviderType.GITHUB: return ( - startFlow(idp.id, ProviderSlug.GITHUB).then( - ({ authUrl }) => { - router.push(authUrl); - }, - ) + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GITHUB), + ).then(({ authUrl }) => { + router.push(authUrl); + }) } > ); - case 7: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITHUB_ES: + case IdentityProviderType.GITHUB_ES: return ( alert("TODO: unimplemented")} > ); - case 5: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_AZURE_AD: + case IdentityProviderType.AZURE_AD: return ( - startFlow(idp.id, ProviderSlug.AZURE).then( - ({ authUrl }) => { - router.push(authUrl); - }, - ) + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.AZURE_AD), + ).then(({ authUrl }) => { + router.push(authUrl); + }) } > ); - case 10: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GOOGLE: + case IdentityProviderType.GOOGLE: return ( - startFlow(idp.id, ProviderSlug.GOOGLE).then( - ({ authUrl }) => { - router.push(authUrl); - }, - ) + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GOOGLE), + ).then(({ authUrl }) => { + router.push(authUrl); + }) } > ); - case 8: // IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITLAB: + case IdentityProviderType.GITLAB: return ( alert("TODO: unimplemented")} > ); - case 9: //IdentityProviderType.IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED: + case IdentityProviderType.GITLAB_SELF_HOSTED: return (
- {loginName} + + {loginName} + {showDropdown && ( { if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: any = { + const sessionCookie: CustomCookieData = { id: createdSession.sessionId, token: createdSession.sessionToken, creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`, @@ -103,7 +102,7 @@ export async function createSessionForUserIdAndUpdateCookie( createdSession.sessionToken, ).then((response) => { if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: any = { + const sessionCookie: CustomCookieData = { id: createdSession.sessionId, token: createdSession.sessionToken, creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`, @@ -153,7 +152,7 @@ export async function createSessionForIdpAndUpdateCookie( createdSession.sessionToken, ).then((response) => { if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: any = { + const sessionCookie: CustomCookieData = { id: createdSession.sessionId, token: createdSession.sessionToken, creationDate: `${response.session.creationDate?.toDate().getTime() ?? ""}`, diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts index 21461498941..f840f3ac048 100644 --- a/packages/zitadel-client/src/v2.ts +++ b/packages/zitadel-client/src/v2.ts @@ -1,10 +1,12 @@ import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_connect"; +import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_connect"; import { RequestContext } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_connect"; import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_connect"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_connect"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_connect"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_connect"; + import { createClientFor } from "./helpers"; export const createUserServiceClient = createClientFor(UserService); @@ -13,6 +15,7 @@ export const createSessionServiceClient = createClientFor(SessionService); export const createOIDCServiceClient = createClientFor(OIDCService); export const createOrganizationServiceClient = createClientFor(OrganizationService); export const createFeatureServiceClient = createClientFor(FeatureService); +export const createIdpServiceClient = createClientFor(IdentityProviderService); export function makeReqCtx(orgId: string | undefined): Partial { return { diff --git a/packages/zitadel-react/src/components/SignInWithAzureAD.tsx b/packages/zitadel-react/src/components/SignInWithAzureAD.tsx index d9f7859bcfd..38acc645912 100644 --- a/packages/zitadel-react/src/components/SignInWithAzureAD.tsx +++ b/packages/zitadel-react/src/components/SignInWithAzureAD.tsx @@ -2,39 +2,20 @@ import { ReactNode, forwardRef } from "react"; import { SignInWithIdentityProviderProps } from "./SignInWith"; +import { IdpButtonClasses } from "./classes"; -export const SignInWithAzureAD = forwardRef< - HTMLButtonElement, - SignInWithIdentityProviderProps ->( +export const SignInWithAzureAD = forwardRef( ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - ), ); diff --git a/packages/zitadel-react/src/components/SignInWithGithub.tsx b/packages/zitadel-react/src/components/SignInWithGithub.tsx index d04f9b5fc0c..435459def55 100644 --- a/packages/zitadel-react/src/components/SignInWithGithub.tsx +++ b/packages/zitadel-react/src/components/SignInWithGithub.tsx @@ -2,18 +2,11 @@ import { ReactNode, forwardRef } from "react"; import { SignInWithIdentityProviderProps } from "./SignInWith"; +import { IdpButtonClasses } from "./classes"; -export const SignInWithGithub = forwardRef< - HTMLButtonElement, - SignInWithIdentityProviderProps ->( +export const SignInWithGithub = forwardRef( ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - ), ); diff --git a/packages/zitadel-react/src/components/SignInWithGitlab.tsx b/packages/zitadel-react/src/components/SignInWithGitlab.tsx index 62b4d04c343..ffb1d09c49c 100644 --- a/packages/zitadel-react/src/components/SignInWithGitlab.tsx +++ b/packages/zitadel-react/src/components/SignInWithGitlab.tsx @@ -1,24 +1,12 @@ import { ReactNode, forwardRef } from "react"; import { SignInWithIdentityProviderProps } from "./SignInWith"; +import { IdpButtonClasses } from "./classes"; -export const SignInWithGitlab = forwardRef< - HTMLButtonElement, - SignInWithIdentityProviderProps ->( +export const SignInWithGitlab = forwardRef( ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - ), ); diff --git a/packages/zitadel-react/src/components/SignInWithGoogle.tsx b/packages/zitadel-react/src/components/SignInWithGoogle.tsx index e3dc0c85965..af2dc40d26d 100644 --- a/packages/zitadel-react/src/components/SignInWithGoogle.tsx +++ b/packages/zitadel-react/src/components/SignInWithGoogle.tsx @@ -1,24 +1,12 @@ import { ReactNode, forwardRef } from "react"; import { SignInWithIdentityProviderProps } from "./SignInWith"; +import { IdpButtonClasses } from "./classes"; -export const SignInWithGoogle = forwardRef< - HTMLButtonElement, - SignInWithIdentityProviderProps ->( +export const SignInWithGoogle = forwardRef( ({ children, className = "", name = "", ...props }, ref): ReactNode => ( - ), ); diff --git a/packages/zitadel-react/src/components/classes.ts b/packages/zitadel-react/src/components/classes.ts new file mode 100644 index 00000000000..11216a561c4 --- /dev/null +++ b/packages/zitadel-react/src/components/classes.ts @@ -0,0 +1,2 @@ +export const IdpButtonClasses = + "transition-all ztdl-w-full ztdl-cursor-pointer ztdl-flex ztdl-flex-row ztdl-items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm";