Merge pull request #83 from zitadel/domain-discovery

feat: domain discovery
This commit is contained in:
Max Peintner
2024-07-30 14:17:13 +02:00
committed by GitHub
14 changed files with 5063 additions and 7133 deletions

5
.prettierrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"printWidth": 125,
"trailingComma": "all",
"plugins": ["prettier-plugin-organize-imports"]
}

View File

@@ -36,10 +36,10 @@
"@heroicons/react": "2.1.3",
"@tailwindcss/forms": "0.5.7",
"@vercel/analytics": "^1.2.2",
"@zitadel/proto": "workspace:*",
"@zitadel/client": "workspace:*",
"@zitadel/react": "workspace:*",
"@zitadel/node": "workspace:*",
"@zitadel/proto": "workspace:*",
"@zitadel/react": "workspace:*",
"clsx": "1.2.1",
"copy-to-clipboard": "^3.3.3",
"moment": "^2.29.4",

View File

@@ -1,14 +1,7 @@
import { ProviderSlug } from "@/lib/demos";
import { getBrandingSettings } from "@/lib/zitadel";
import { getBrandingSettings, PROVIDER_NAME_MAPPING } from "@/lib/zitadel";
import DynamicTheme from "@/ui/DynamicTheme";
const PROVIDER_NAME_MAPPING: {
[provider: string]: string;
} = {
[ProviderSlug.GOOGLE]: "Google",
[ProviderSlug.GITHUB]: "GitHub",
};
export default async function Page({
searchParams,
params,

View File

@@ -51,6 +51,9 @@ export default async function Page({
<RegisterFormWithoutPassword
legal={legal}
organization={organization}
firstname={firstname}
lastname={lastname}
email={email}
authRequestId={authRequestId}
></RegisterFormWithoutPassword>
)}

View File

@@ -1,12 +1,21 @@
import { listAuthenticationMethodTypes, listUsers } from "@/lib/zitadel";
import { ProviderSlug } from "@/lib/demos";
import {
getActiveIdentityProviders,
getLoginSettings,
listAuthenticationMethodTypes,
listUsers,
PROVIDER_NAME_MAPPING,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { createSessionForUserIdAndUpdateCookie } from "@/utils/session";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2beta/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((users) => {
return listUsers(loginName, organization).then(async (users) => {
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
const userId = users.result[0].userId;
return createSessionForUserIdAndUpdateCookie(
@@ -36,7 +45,88 @@ export async function POST(request: NextRequest) {
console.error(error);
return NextResponse.json(error, { status: 500 });
});
} else {
} else if (organization) {
const loginSettings = await getLoginSettings(organization);
// TODO: check if allowDomainDiscovery has to be allowed too, to redirect to the register page
// user not found, check if register is enabled on organization
if (
loginSettings?.allowRegister &&
!loginSettings?.allowUsernamePassword
) {
// TODO redirect to loginname page with idp hint
const identityProviders = await getActiveIdentityProviders(
organization,
).then((resp) => {
return resp.identityProviders;
});
if (identityProviders.length === 1) {
const host = request.nextUrl.origin;
const provider =
identityProviders[0].type === IdentityProviderType.GITHUB
? "github"
: identityProviders[0].type === IdentityProviderType.GOOGLE
? "google"
: identityProviders[0].type === IdentityProviderType.AZURE_AD
? "azure"
: identityProviders[0].type === IdentityProviderType.SAML
? "saml"
: identityProviders[0].type === IdentityProviderType.OIDC
? "oidc"
: "oidc";
const params = new URLSearchParams();
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
if (organization) {
params.set("organization", organization);
}
return startIdentityProviderFlow({
idpId: identityProviders[0].id,
urls: {
successUrl:
`${host}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${host}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp: any) => {
if (resp.authUrl) {
return NextResponse.json({ nextStep: resp.authUrl });
}
});
}
} else if (
loginSettings?.allowRegister &&
loginSettings?.allowUsernamePassword
) {
const params: any = { organization };
if (authRequestId) {
params.authRequestId = authRequestId;
}
if (loginName) {
params.email = loginName;
}
const registerUrl = new URL(
"/register?" + new URLSearchParams(params),
request.url,
);
return NextResponse.json({
nextStep: registerUrl,
status: 200,
});
}
return NextResponse.json(
{ message: "Could not find user" },
{ status: 404 },

View File

@@ -1,8 +1,10 @@
import {
createCallback,
getActiveIdentityProviders,
getAuthRequest,
getOrgByDomain,
listSessions,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { SessionCookie, getAllSessions } from "@/utils/cookies";
import { NextRequest, NextResponse } from "next/server";
@@ -11,6 +13,7 @@ import {
AuthRequest,
Prompt,
} from "@zitadel/proto/zitadel/oidc/v2beta/authorization_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2beta/login_settings_pb";
async function loadSessions(ids: string[]): Promise<Session[]> {
const response = await listSessions(
@@ -22,6 +25,7 @@ async function loadSessions(ids: string[]): Promise<Session[]> {
const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/;
const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options
const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/;
function findSession(
sessions: Session[],
@@ -100,12 +104,17 @@ export async function GET(request: NextRequest) {
const { authRequest } = await getAuthRequest({ authRequestId });
let organization = "";
let idpId = "";
if (authRequest?.scope) {
const orgScope = authRequest.scope.find((s: string) =>
ORG_SCOPE_REGEX.test(s),
);
const idpScope = authRequest.scope.find((s: string) =>
IDP_SCOPE_REGEX.test(s),
);
if (orgScope) {
const matched = ORG_SCOPE_REGEX.exec(orgScope);
organization = matched?.[1] ?? "";
@@ -123,6 +132,62 @@ export async function GET(request: NextRequest) {
}
}
}
if (idpScope) {
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
const identityProviders = await getActiveIdentityProviders(
organization,
).then((resp) => {
return resp.identityProviders;
});
const idp = identityProviders.find((idp) => idp.id === idpId);
if (idp) {
const host = request.nextUrl.origin;
const provider =
idp.type === IdentityProviderType.GITHUB
? "github"
: identityProviders[0].type === IdentityProviderType.GOOGLE
? "google"
: identityProviders[0].type === IdentityProviderType.AZURE_AD
? "azure"
: identityProviders[0].type === IdentityProviderType.SAML
? "saml"
: identityProviders[0].type === IdentityProviderType.OIDC
? "oidc"
: "oidc";
const params = new URLSearchParams();
if (authRequestId) {
params.set("authRequestId", authRequestId);
}
if (organization) {
params.set("organization", organization);
}
return startIdentityProviderFlow({
idpId,
urls: {
successUrl:
`${host}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${host}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp: any) => {
if (resp.authUrl) {
return NextResponse.redirect(resp.authUrl);
}
});
}
}
}
const gotoAccounts = (): NextResponse<unknown> => {

View File

@@ -7,15 +7,18 @@ import {
} from "@zitadel/client/v2beta";
import { createManagementServiceClient } from "@zitadel/client/v1";
import { createServerTransport } from "@zitadel/node";
import { GetActiveIdentityProvidersRequest } from "@zitadel/proto/zitadel/settings/v2beta/settings_service_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2beta/session_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2beta/challenge_pb";
import {
RetrieveIdentityProviderIntentRequest,
VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2beta/user_service_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2beta/oidc_service_pb";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2beta/object_pb";
import type { RedirectURLs } from "@zitadel/proto/zitadel/user/v2beta/idp_pb";
import { ProviderSlug } from "./demos";
import { PlainMessage } from "@zitadel/client";
const SESSION_LIFETIME_S = 3000;
@@ -290,6 +293,13 @@ export async function getOrgByDomain(domain: string) {
return managementService.getOrgByDomainGlobal({ domain }, {});
}
export const PROVIDER_NAME_MAPPING: {
[provider: string]: string;
} = {
[ProviderSlug.GOOGLE]: "Google",
[ProviderSlug.GITHUB]: "GitHub",
};
export async function startIdentityProviderFlow({
idpId,
urls,
@@ -426,6 +436,13 @@ export async function verifyU2FRegistration(
return userService.verifyU2FRegistration(request, {});
}
export async function getActiveIdentityProviders(orgId: string) {
return settingsService.getActiveIdentityProviders(
{ ctx: makeReqCtx(orgId) },
{},
);
}
/**
*
* @param userId the id of the user where the email should be set

View File

@@ -13,6 +13,7 @@ import AuthenticationMethodRadio, {
import Alert from "./Alert";
import BackButton from "./BackButton";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2beta/legal_settings_pb";
import { first } from "node_modules/cypress/types/lodash";
type Inputs =
| {
@@ -24,17 +25,28 @@ type Inputs =
type Props = {
legal: LegalAndSupportSettings;
firstname?: string;
lastname?: string;
email?: string;
organization?: string;
authRequestId?: string;
};
export default function RegisterFormWithoutPassword({
legal,
email,
firstname,
lastname,
organization,
authRequestId,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
email: email ?? "",
firstName: firstname ?? "",
lastname: lastname ?? "",
},
});
const [loading, setLoading] = useState<boolean>(false);

View File

@@ -42,6 +42,11 @@ export default function SetPasswordForm({
}: Props) {
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
mode: "onBlur",
defaultValues: {
email: email ?? "",
firstname: firstname ?? "",
lastname: lastname ?? "",
},
});
const [loading, setLoading] = useState<boolean>(false);

View File

@@ -19,18 +19,13 @@ export interface SignInWithIDPProps {
identityProviders: IdentityProvider[];
authRequestId?: string;
organization?: string;
startIDPFlowPath?: (idpId: string) => string;
}
const START_IDP_FLOW_PATH = (idpId: string) =>
`/v2beta/users/idps/${idpId}/start`;
export function SignInWithIDP({
host,
identityProviders,
authRequestId,
organization,
startIDPFlowPath = START_IDP_FLOW_PATH,
}: SignInWithIDPProps) {
// TODO: remove casting when bufbuild/protobuf-es@v2 is released
identityProviders = identityProviders.map((idp) =>

View File

@@ -82,9 +82,10 @@ export default function UsernameForm({
values: Inputs,
organization?: string,
) {
console.log(loginSettings);
return submitLoginName(values, organization).then((response) => {
if (response.authMethodTypes.length == 1) {
if (response.nextStep) {
return router.push(response.nextStep);
} else if (response.authMethodTypes.length == 1) {
const method = response.authMethodTypes[0];
switch (method) {
case 1: // user has only password as auth method
@@ -92,8 +93,11 @@ export default function UsernameForm({
loginName: response.factors.user.loginName,
};
if (organization) {
paramsPassword.organization = organization;
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
if (organization || response.factors.user.organizationId) {
paramsPassword.organization =
organization ?? response.factors.user.organizationId;
}
if (
@@ -117,8 +121,10 @@ export default function UsernameForm({
if (authRequestId) {
paramsPasskey.authRequestId = authRequestId;
}
if (organization) {
paramsPasskey.organization = organization;
if (organization || response.factors.user.organizationId) {
paramsPasskey.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -134,8 +140,10 @@ export default function UsernameForm({
if (authRequestId) {
paramsPasskeyDefault.authRequestId = authRequestId;
}
if (organization) {
paramsPasskeyDefault.organization = organization;
if (organization || response.factors.user.organizationId) {
paramsPasskeyDefault.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -161,8 +169,9 @@ export default function UsernameForm({
passkeyParams.authRequestId = authRequestId;
}
if (organization) {
passkeyParams.organization = organization;
if (organization || response.factors.user.organizationId) {
passkeyParams.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(
@@ -180,8 +189,9 @@ export default function UsernameForm({
paramsPasswordDefault.authRequestId = authRequestId;
}
if (organization) {
paramsPasswordDefault.organization = organization;
if (organization || response.factors.user.organizationId) {
paramsPasswordDefault.organization =
organization ?? response.factors.user.organizationId;
}
return router.push(

View File

@@ -29,6 +29,7 @@
"eslint": "^8.57.0",
"eslint-config-zitadel": "workspace:*",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"tsup": "^8.0.2",
"turbo": "2.0.9",
"typescript": "^5.4.5",

1
packages/zitadel-client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
src/proto

11939
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff