ppresume / community

Discussions, feedbacks, roadmaps, community.
59 stars 3 forks source link

feat: auth: social sign in with Google #7

Closed xiaohanyu closed 6 months ago

xiaohanyu commented 11 months ago

Description

As title, we want to support social sign in with Google

[Optional] Possible solutions

NA

Acceptance Criteria

Todo list

- [x] check google documentation to see how to support social sign in with google account
- [x] implement the logic for social sign in with google account
xiaohanyu commented 10 months ago

After some evaluation we found that this is a bit hard to implement right.

Strapi (yes we use strapi in our backend) does not support one user to use multiple auth providers right now, check the issue and form.

There're some essential trickies to get it right here.

Let's say that we have a user registered via plain credentials, with username foo and email foo@example.com, then another user want to sign in via Google OAuth, with gmail account foo@gmail.com, then it is highly likely that the system would lead to duplicated username of foo, which would break lots of things.

Other CMS like [directus] seems have similar issue.

Let's postpone this issue and see whether strapi official could come up with better solution.

xiaohanyu commented 10 months ago

Another issue in strapi github: https://github.com/strapi/strapi/issues/8807

xiaohanyu commented 6 months ago

This is finally implemented and put online with the help of logto:

google-sign-in google-sign-in-oauth-consent-screen google-sign-in-account-linking
Davidosky007 commented 6 months ago

@xiaohanyu please how can this solution be implemented, i currently face this issue

xiaohanyu commented 6 months ago

@xiaohanyu please how can this solution be implemented, i currently face this issue

Hey, @Davidosky007 , do you use strapi as well? I decided to mgirate from strapi to payload in the end (for details, check my tweet here).

However the rationale is the same. Basically, many headless CMS have the same issue with OAuth, i.e, one email multiple account issue.

My solution here is to get rid of the internal, builtin auth of any CMS and then adopt a dedicated auth server, in my case, I chose logto, which provides a useful account linking feature that solves exactly one email multiple accounts issue.

For the story of introducing the new auth flow for PPResume, check the blog post.

Besides, I will list more tech details here, just FYI:

So basically, the architecture of current PPResume:

For example, I have the following definitions in payload for resumes:

export const Resumes: CollectionConfig = {
  slug: 'resumes',
  admin: {
    useAsTitle: 'title',
    defaultColumns: [
      'id',
      'title',
      'ownerId',
      'deleted',
      'createdAt',
      'updatedAt',
    ],
  },
  access: {
    read: ({ req }) => {
      return isAdmin(req) || isOwner(req)
    },
    create: ({ req }) => {
      return isAdmin(req) || isCustomer(req)
    },
    update: ({ req }) => {
      return isAdmin(req) || isOwner(req)
    },
  }
  // ...
}

Here I have several auth check functions to different endpoints:

A sample definition for isCustomer:

/**
 * Extracts the Bearer Token from the Authorization header.
 */
function extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders) {
  const bearerTokenIdentifier = 'Bearer'

  if (!authorization) {
    throw new Error('auth.authorization_header_missing')
  }

  if (!authorization.startsWith(bearerTokenIdentifier)) {
    throw new Error('auth.authorization_token_type_not_supported')
  }

  return authorization.slice(bearerTokenIdentifier.length + 1)
}

/**
 * Parse and verify the JWT token from the request.
 */
export async function verifyAuthFromRequest(
  req: IncomingMessage
): Promise<JWTPayload> {
  const bearerToken = extractBearerTokenFromHeaders(req.headers)

  const { payload } = await jwtVerify(
    bearerToken,
    // generate a jwks using jwks_uri inquired from Logto server
    createRemoteJWKSet(new URL(`${process.env.LOGTO_ENDPOINT}/oidc/jwks`)),
    {
      // expected issuer of the token, should be issued by the Logto server
      issuer: `${process.env.LOGTO_ENDPOINT}/oidc`,
      // expected audience token, should be the resource indicator of the current API
      audience: process.env.PAYLOAD_RESUMES_RESOURCE,
    }
  )

  return payload
}

/**
 * Check whether the user is a valid customer.
 *
 * We only check whether the user has a valid JWT token issued by Logto server.
 *
 * @param req - The payload request object.
 * @returns a valid JWT payload if the user is a valid customer.
 */
export async function isCustomer(req: PayloadRequest): Promise<JWTPayload> {
  try {
    const payload = await verifyAuthFromRequest(req)
    return payload
  } catch (err) {
    console.error('Failed to verify auth from request, error: ', err)
  }
}

For how to integrate logto with frontend and backend, you can check logto docs.

But still, there are many intricacies here. If you want to know more, I guess I can write some blog posts for this topic.