Skip to content

像官方 ChatGPT 一样使用,再加入更强大的功能(Use as the official ChatGPT, with even more powerful features.)

Notifications You must be signed in to change notification settings

sirily11/chatgpt-nextjs

 
 

Repository files navigation

ChatGPT Custom UI

Create a Password less registration experience

Install required dependencies

  1. @passwordless-id/webauthn: Handle the server side and client side of the WebAuthn protocol
  2. @vercel/kv: Handle the registration and login sessions
  3. firebase-admin: Handle the user database and store the credentials generated by `@passwordless-id/webauthn
  4. uuid: Generate a unique challenge for each registration and login request
  5. zod: Validate the data sent by the client

Create a challenge api route

First, we need to create an api route that will generate a challenge for each registration and login request, and it will also store the challenge in the KV store.

import {
  KvStoreTypeSchema,
  getKey
} from '@/service/firebase/webauthn/kv.utils';
import { v4 } from 'uuid';
import { kv } from '@vercel/kv';
import { z } from 'zod';
import { NextResponse } from 'next/server';
import { config } from '@/config';

export const runtime = 'edge';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  try {
    // get challenge type and user id from query params
    const storeType = KvStoreTypeSchema.parse(searchParams.get('type'));
    const userId = z.string().parse(searchParams.get('userId'));
    // generate a challenge
    const challenge = v4();
    // get the key this challenge
    const kvKey = getKey(storeType, userId);
    if (await kv.exists(kvKey)) {
      const data = await kv.get(kvKey);
      return NextResponse.json(data);
    }

    // store the challenge in the kv store
    await kv.set(kvKey, JSON.stringify({ challenge }));
    await kv.expire(kvKey, config.defaultKvExpiration);

    return NextResponse.json({ challenge });
  } catch (err) {
    // check if the error is a zod error
    if (err instanceof z.ZodError) {
      return NextResponse.json({ error: err.errors }, { status: 400 });
    }
    throw err;
  }
}

Create a registration route

Before creating the route, we need to define the firestore data structure.

users
└── username
    └── credential
        ├── id
        ├── publicKey
        ├── algorithm

Then we can create the route. This route will verify the registration request.

This also writes the credential to the firebase database and creates a user in the firebase auth.

import { getAdmin } from '@/api/firebase';
import { getKey } from '@/service/firebase/webauthn/kv.utils';
import { server } from '@passwordless-id/webauthn';
import { kv } from '@vercel/kv';
import { NextResponse } from 'next/server';
import { z } from 'zod';

const RequestSchema = z.object({
  username: z.string(),
  credential: z.object({
    id: z.string(),
    publicKey: z.string(),
    algorithm: z.enum(['RS256', 'ES256'])
  }),
  authenticatorData: z.string(),
  clientData: z.string()
});

export async function POST(request: Request) {
  const registration = RequestSchema.parse(await request.json());
  const key = getKey('registration', registration.username);
  if (!kv.exists(key)) {
    return NextResponse.json(
      { error: 'No registration session found' },
      { status: 400 }
    );
  }

  const session = z
    .object({
      challenge: z.string()
    })
    .parse(await kv.get(key));
  await server.verifyRegistration(registration, {
    challenge: session.challenge,
    origin: () => true
  });
  // store the credential in the firebase
  const admin = getAdmin();
  // check if the user already exists
  const user = await admin
    .firestore()
    .collection('users')
    .doc(registration.username)
    .get();

  if (user.exists) {
    return NextResponse.json(
      { message: 'User already exists' },
      { status: 400 }
    );
  }

  await admin.firestore().collection('users').doc(registration.username).set({
    credential: registration.credential
  });
  // create a user in the firebase auth
  await admin.auth().createUser({
    uid: registration.credential.id,
    displayName: registration.username
  });
  return NextResponse.json({ id: registration.username });
}

Write client side registration logic

First, we need to write a function to get the challenge from the api route we created earlier.

static async getChallenge(
    type: KvStoreType,
    userId: string
  ): Promise<string> {
    const response = await axios.get(
      `/api/challenge?type=${type}&userId=${userId}`
    );
    const data = ChallengeSchema.parse(response.data);
    return data.challenge;
  }

Then we can use the challenge to create a credential request and send it to the server.

export class PasswordlessAuthenticationService {
  static async getChallenge(
    type: KvStoreType,
    userId: string
  ): Promise<string> {
    const response = await axios.get(
      `/api/challenge?type=${type}&userId=${userId}`
    );
    const data = ChallengeSchema.parse(response.data);
    return data.challenge;
  }

  static async registration(username: string) {
    // check if username greater than 3 characters
    if (username.length < 3) {
      throw new Error('Username must be greater than 3 characters');
    }

    if (!client.isAvailable()) {
      throw new Error('WebAuthn is not available');
    }
    // since we don't have the user id yet, we'll use the username as the user id
    const challenge = await this.getChallenge('registration', username);
    const credential = await client.register(username, challenge);
    await axios.post('/api/auth/registration', credential);
  }
}

About

像官方 ChatGPT 一样使用,再加入更强大的功能(Use as the official ChatGPT, with even more powerful features.)

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 94.6%
  • CSS 3.7%
  • Other 1.7%