passkey verify, page styles

This commit is contained in:
Max Peintner
2023-06-15 13:58:32 +02:00
parent 69301d789d
commit 73d5c6e70c
12 changed files with 1791 additions and 1586 deletions

View File

@@ -1,40 +1,45 @@
version: '3.8'
version: "3.8"
services:
zitadel:
user: '${ZITADEL_DEV_UID}'
image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}'
user: "${ZITADEL_DEV_UID}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}"
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
ports:
- "8080:8080"
- "8080:8080"
volumes:
- ./machinekey:/machinekey
- ./zitadel.yaml:/zitadel.yaml
depends_on:
db:
condition: 'service_healthy'
condition: "service_healthy"
db:
image: 'cockroachdb/cockroach:v22.2.2'
command: 'start-single-node --insecure --http-addr :9090'
image: "cockroachdb/cockroach:v22.2.2"
command: "start-single-node --insecure --http-addr :9090"
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:9090/health?ready=1']
interval: '10s'
timeout: '30s'
test: ["CMD", "curl", "-f", "http://localhost:9090/health?ready=1"]
interval: "10s"
timeout: "30s"
retries: 5
start_period: '20s'
start_period: "20s"
ports:
- "26257:26257"
- "9090:9090"
wait_for_zitadel:
image: curlimages/curl:8.00.1
command: [ "/bin/sh", "-c", "i=0; while ! curl http://zitadel:8080/debug/ready && [ $$i -lt 30 ]; do sleep 1; i=$$((i+1)); done; [ $$i -eq 30 ] && exit 1 || exit 0" ]
command:
[
"/bin/sh",
"-c",
"i=0; while ! curl http://zitadel:8080/debug/ready && [ $$i -lt 30 ]; do sleep 1; i=$$((i+1)); done; [ $$i -eq 30 ] && exit 1 || exit 0",
]
depends_on:
- zitadel
setup:
user: '${ZITADEL_DEV_UID}'
user: "${ZITADEL_DEV_UID}"
container_name: setup
build: .
environment:
@@ -42,8 +47,8 @@ services:
SERVICE: http://zitadel:8080
WRITE_ENVIRONMENT_FILE: /apps/login/.env.local
volumes:
- "./machinekey:/key"
- "../apps/login:/apps/login"
- "./machinekey:/key"
- "../apps/login:/apps/login"
depends_on:
wait_for_zitadel:
condition: 'service_completed_successfully'
condition: "service_completed_successfully"

View File

@@ -24,14 +24,31 @@ export default async function Page({
console.log(sessionFactors);
return (
<div className="flex flex-col items-center space-y-4">
<h1>Register Passkey</h1>
<h1>Use your passkey to confirm its really you</h1>
{sessionFactors && (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName ?? ""}
displayName={sessionFactors.factors?.user?.displayName}
showDropdown
></UserAvatar>
)}
<p className="ztdl-p mb-6 block">
Setup your user to authenticate with passkeys.
Your device will ask for your fingerprint, face, or screen lock
</p>
<Alert type={AlertType.INFO}>
A passkey is an authentication method on a device like your fingerprint,
Apple FaceID or similar.
<span>
A passkey is an authentication method on a device like your
fingerprint, Apple FaceID or similar.{" "}
<a
className="text-primary-light-500 dark:text-primary-dark-500 hover:text-primary-light-300 hover:dark:text-primary-dark-300"
target="_blank"
href="https://zitadel.com/docs/guides/manage/user/reg-create-user#with-passwordless"
>
Passwordless Authentication
</a>
</span>
</Alert>
{!sessionFactors && (
@@ -43,14 +60,6 @@ export default async function Page({
</div>
)}
{sessionFactors && (
<UserAvatar
loginName={loginName ?? sessionFactors.factors?.user?.loginName ?? ""}
displayName={sessionFactors.factors?.user?.displayName}
showDropdown
></UserAvatar>
)}
{sessionFactors?.id && <RegisterPasskey sessionId={sessionFactors.id} />}
</div>
);

View File

@@ -23,14 +23,16 @@ export async function POST(request: NextRequest) {
const userId = session?.session?.factors?.user?.id;
if (userId) {
return createPasskeyRegistrationLink(userId, sessionCookie.token)
return createPasskeyRegistrationLink(userId)
.then((resp) => {
const code = resp.code;
console.log("code", code);
return registerPasskey(userId, code).then((resp) => {
return NextResponse.json(resp);
});
})
.catch((error) => {
console.log("error on creating passkey registration link");
return NextResponse.json(error, { status: 500 });
});
} else {
@@ -40,6 +42,6 @@ export async function POST(request: NextRequest) {
);
}
} else {
return NextResponse.json({}, { status: 500 });
return NextResponse.json({}, { status: 400 });
}
}

View File

@@ -0,0 +1,51 @@
import {
createPasskeyRegistrationLink,
getSession,
registerPasskey,
server,
verifyPasskeyRegistration,
} from "#/lib/zitadel";
import { getSessionCookieById } from "#/utils/cookies";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
if (body) {
const { passkeyId, passkeyName, publicKeyCredential, sessionId } = body;
const sessionCookie = await getSessionCookieById(sessionId);
const session = await getSession(
server,
sessionCookie.id,
sessionCookie.token
);
const userId = session?.session?.factors?.user?.id;
if (userId) {
return verifyPasskeyRegistration(
server,
passkeyId,
passkeyName,
publicKeyCredential,
userId
)
.then((resp) => {
console.log("verifyresponse", resp);
return NextResponse.json(resp);
})
.catch((error) => {
console.log("error on verifying passkey");
return NextResponse.json(error, { status: 500 });
});
} else {
return NextResponse.json(
{ details: "could not get session" },
{ status: 500 }
);
}
} else {
return NextResponse.json({}, { status: 400 });
}
}

View File

@@ -20,7 +20,9 @@ import {
VerifyEmailResponse,
SetSessionResponse,
DeleteSessionResponse,
VerifyPasskeyRegistrationRequest,
VerifyPasskeyRegistrationResponse,
orgMetadata,
} from "@zitadel/server";
import { Metadata } from "nice-grpc";
@@ -196,26 +198,14 @@ const bearerTokenMetadata = (token: string) =>
* @returns the newly set email
*/
export async function createPasskeyRegistrationLink(
userId: string,
sessionToken: string
userId: string
): Promise<any> {
// this actions will be made from the currently seleected user
// const zitadelConfig: ZitadelServerOptions = {
// name: "zitadel login",
// apiUrl: process.env.ZITADEL_API_URL ?? "",
// token: "",
// };
// const authserver: ZitadelServer = initializeServer(zitadelConfig);
// console.log("server", authserver);
const userservice = user.getUser(server);
return userservice.createPasskeyRegistrationLink(
{
userId,
returnCode: {},
}
// { metadata: bearerTokenMetadata(sessionToken) }
);
return userservice.createPasskeyRegistrationLink({
userId,
returnCode: {},
});
}
/**
@@ -228,10 +218,15 @@ export async function verifyPasskeyRegistration(
server: ZitadelServer,
passkeyId: string,
passkeyName: string,
publicKeyCredential: any,
publicKeyCredential:
| {
[key: string]: any;
}
| undefined,
userId: string
): Promise<VerifyPasskeyRegistrationResponse> {
const userservice = user.getUser(server);
console.log(passkeyId, passkeyName, publicKeyCredential, userId);
return userservice.verifyPasskeyRegistration(
{
passkeyId,

View File

@@ -9,7 +9,7 @@
@layer base {
h1,
.ztdl-h1 {
@apply text-2xl;
@apply text-2xl text-center;
}
.ztdl-p {

View File

@@ -1,4 +1,7 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import {
ExclamationTriangleIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import clsx from "clsx";
type Props = {
@@ -29,8 +32,13 @@ export default function Alert({ children, type = AlertType.ALERT }: Props) {
}
)}
>
<ExclamationTriangleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" />
<span className="text-center text-sm">{children}</span>
{type === AlertType.ALERT && (
<ExclamationTriangleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" />
)}
{type === AlertType.INFO && (
<InformationCircleIcon className="flex-shrink-0 h-5 w-5 mr-2 ml-2" />
)}
<span className="text-sm">{children}</span>
</div>
);
}

View File

@@ -50,9 +50,40 @@ export default function RegisterPasskey({ sessionId }: Props) {
return response;
}
async function submitVerify(
passkeyId: string,
passkeyName: string,
publicKeyCredential: any,
sessionId: string
) {
setLoading(true);
const res = await fetch("/passkeys/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
passkeyId,
passkeyName,
publicKeyCredential,
sessionId,
}),
});
const response = await res.json();
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response.details);
}
return response;
}
function submitRegisterAndContinue(value: Inputs): Promise<boolean | void> {
return submitRegister().then((resp: RegisterPasskeyResponse) => {
console.log(resp.publicKeyCredentialCreationOptions?.publicKey);
const passkeyId = resp.passkeyId;
console.log("passkeyId", passkeyId);
if (
resp.publicKeyCredentialCreationOptions &&
resp.publicKeyCredentialCreationOptions.publicKey
@@ -96,10 +127,11 @@ export default function RegisterPasskey({ sessionId }: Props) {
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = JSON.stringify({
const data = {
id: resp.id,
rawId: coerceToBase64Url(rawId, "rawId"),
type: resp.type,
// response: (resp as any).response,
response: {
attestationObject: coerceToBase64Url(
attestationObject,
@@ -110,11 +142,10 @@ export default function RegisterPasskey({ sessionId }: Props) {
"clientDataJSON"
),
},
});
};
console.log(data);
const base64 = btoa(data);
return base64;
return submitVerify(passkeyId, "name", data, sessionId);
// if (this.type === U2FComponentDestination.MFA) {
// this.service
// .verifyMyMultiFactorU2F(base64, this.name)

View File

@@ -14,7 +14,7 @@ export default function UserAvatar({
showDropdown,
}: Props) {
return (
<div className="flex h-full w-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
<div className="flex h-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
<div>
<Avatar
size="small"
@@ -27,7 +27,7 @@ export default function UserAvatar({
{showDropdown && (
<Link
href="/accounts"
className="flex items-center justify-center p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full mr-1 transition-all"
className="ml-4 flex items-center justify-center p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded-full mr-1 transition-all"
>
<ChevronDownIcon className="h-4 w-4" />
</Link>

View File

@@ -18,7 +18,7 @@
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"prettier": "^2.5.1",
"turbo": "^1.9.8"
"turbo": "^1.10.3"
},
"packageManager": "pnpm@7.15.0"
}

View File

@@ -28,8 +28,12 @@ export {
export {
AddHumanUserResponse,
VerifyEmailResponse,
VerifyPasskeyRegistrationRequest,
VerifyPasskeyRegistrationResponse,
RegisterPasskeyRequest,
RegisterPasskeyResponse,
CreatePasskeyRegistrationLinkResponse,
CreatePasskeyRegistrationLinkRequest,
} from "./proto/server/zitadel/user/v2alpha/user_service";
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings";

3148
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff