Skip to content

无需前端,2 个接口实现 OAuth 认证

发布于  at 15:34

看过了太多开发者设计的 OAuth 流程,有很多人竟然需要前端参与开发,渲染前端页面本身就是很复杂的流程了,竟然还要反复渲染页面,性能极差、可读性极差、又浪费人力,我实在是看不下去了。

OAuth 这个流程完全只需要依赖 HTTP 状态流转就行了,完全不需要前端页面处理,而且性能极好,实测 200ms 内就能完成认证过程。前端参与的那种方案,每次都渲染页面都至少一秒多了,加起来至少也得五秒了,这还是页面性能及格的情况下。。。整个世界就是一个大草台班子!

流程

代码实现

import { Controller, Get, Query, Response } from "@nestjs/common";
import { Response as RawResponse } from "express";

@Controller("oauth")
export class OAuthController {
  constructor(private readonly authService: AuthService) {}

  @Get("login")
  async redirectToLogin(@Response() res: RawResponse, @Query("back") back = "/", @Query("platform") platform: string) {
    console.log(`👨🏻‍💻 login by ${platform}`);
    let state = back;
    let loginUrl: string;
    const redirect_uri = `${process.env.SELF_URL}/api/oauth/redirect?platform=${platform}`;
    switch (platform) {
      case OAuthPlatform.Google:
        loginUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
          process.env.GOOGLE_CLIENT_ID
        }&redirect_uri=${encodeURIComponent(
          redirect_uri
        )}&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/userinfo.profile&state=${state}`;
        break;
      case OAuthPlatform.GitHub:
        loginUrl = `https://github.com/login/oauth/authorize?client_id=${
          process.env.GITHUB_CLIENT_ID
        }&redirect_uri=${encodeURIComponent(redirect_uri)}&state=${state}&scope=read:user,user:email`;
        break;
      case OAuthPlatform.GitLab:
        loginUrl = `${gitlabConstants.instanceUrl}/oauth/authorize?client_id=${
          gitlabConstants.oauth.client_id
        }&redirect_uri=${encodeURIComponent(
          redirect_uri
        )}&response_type=code&state=${state}&scope=read_user+email&code_challenge=${
          challenge.code_challenge
        }&code_challenge_method=S256`;
        break;
      // case 'wechat':
      //   loginUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=${
      //     process.env.WECHAT_APPID
      //   }&redirect_uri=${encodeURIComponent(
      //     redirect_uri,
      //   )}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
      //   break;
      default:
        res.status(400).send("unknown platform:" + platform);
        return;
    }
    if (loginUrl) {
      console.log("redirect to:", loginUrl);
      res.redirect(302, loginUrl);
    }
  }

  @Get("redirect")
  async redirect(
    @Query("code") code: string,
    @Query("state") state = "/",
    @Query("platform") platform: OAuthPlatform,
    @Query("error_description") error_description: string,
    @Response() res: RawResponse
  ) {
    if (error_description) {
      res.status(400).send(error_description);
      return;
    }

    let backUrl = decodeURIComponent(state);

    let user: ReqUserDTO;
    if (code) {
      console.log(`👨🏻‍💻 ${platform} oauth`);
      let redirect_uri = `${process.env.SELF_URL}/api/oauth/redirect?platform=${platform}`;
      switch (platform) {
        case OAuthPlatform.Google:
          try {
            const rez = await request<{
              access_token: string;
            }>("https://oauth2.googleapis.com/token", {
              // https://developers.google.com/identity/protocols/oauth2/web-server#httprest_1
              method: "POST",
              query: {
                client_id: process.env.GOOGLE_CLIENT_ID,
                client_secret: process.env.GOOGLE_CLIENT_SECRET,
                redirect_uri,
                code,
                grant_type: "authorization_code",
              },
            });
            console.log("got google access_token", rez);
            const googleUser = await request<{
              email: string;
              name: string;
              picture: string;
              verified_email: boolean;
            }>("https://www.googleapis.com/oauth2/v1/userinfo", {
              headers: { Authorization: `Bearer ${rez.access_token}` },
            });
            console.log("got google user", googleUser);
            if (!googleUser.verified_email) {
              res.status(400).send("email not verified");
              return;
            }
            user = await this.authService.upsertUser(
              {
                avatar: googleUser.picture,
                name: googleUser.name,
                email: googleUser.email,
              },
              {
                platform,
                // exUserId: googleUser.email,
                exUserName: googleUser.name,
                exUserIdStr: googleUser.email,
                accessToken: rez.access_token,
              },
              inviteCode
            );
          } catch (error) {
            res.status(500).send(error2String(error));
            console.error(error);
            return;
          }
          break;
        case OAuthPlatform.GitHub:
          try {
            const rez = await request<{
              access_token: string;
              scope: string;
              token_type: string;
            }>("https://github.com/login/oauth/access_token", {
              // https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
              method: "POST",
              query: {
                client_id: process.env.GITHUB_CLIENT_ID,
                client_secret: process.env.GITHUB_CLIENT_SECRET,
                redirect_uri,
                code,
              },
              headers: {
                Accept: "application/json",
              },
            });
            console.log("got github access_token");
            const githubUser = await request<GitHubUser>("https://api.github.com/user", {
              headers: { Authorization: `Bearer ${rez.access_token}` },
            });
            // console.log(githubUser);
            if (!githubUser.email) {
              console.log("no public email, try to get verified email");
              const emails = await request<{ verified: boolean; email: string; primary: true }[]>(
                "https://api.github.com/user/emails",
                {
                  headers: { Authorization: `Bearer ${rez.access_token}` },
                }
              );
              // console.log(emails);
              const primaryEmail = emails.find(email => email.primary);
              if (primaryEmail) {
                githubUser.email = primaryEmail.email;
              } else {
                githubUser.email = emails.find(email => email.verified)?.email;
              }
            }
            user = await this.authService.upsertUser(
              {
                avatar: githubUser.avatar_url,
                name: githubUser.name,
                email: githubUser.email,
              },
              {
                platform,
                exUserId: githubUser.id,
                exUserName: githubUser.name,
                exUserIdStr: githubUser.login,
                accessToken: rez.access_token,
              },
              inviteCode
            );
          } catch (error) {
            res.status(500).send(error2String(error));
            console.error(error);
            return;
          }
          break;
        case OAuthPlatform.GitLab:
          try {
            const rez = await request<{
              access_token: string;
              refresh_token: string;
              token_type: string;
              created_at: number;
            }>(gitlabConstants.instanceUrl + "/oauth/token", {
              // https://docs.gitlab.com/ee/api/oauth2.html
              method: "POST",
              query: {
                ...gitlabConstants.oauth,
                redirect_uri,
                code,
                grant_type: "authorization_code",
                code_verifier: challenge.code_verifier,
              },
            });
            console.log("got gitlab access_token");
            const gitlabUser = await request<GitLabUser>(gitlabConstants.instanceUrl + "/api/v4/user", {
              headers: { Authorization: `Bearer ${rez.access_token}` },
            });
            user = await this.authService.upsertUser(
              {
                avatar: gitlabUser.avatar_url,
                name: gitlabUser.name,
                email: gitlabUser.email,
              },
              {
                platform,
                exUserId: gitlabUser.id,
                exUserName: gitlabUser.name,
                exUserIdStr: gitlabUser.username,
                accessToken: rez.access_token,
                refreshToken: rez.refresh_token,
              },
              inviteCode
            );
          } catch (error) {
            console.error(error);
          }
          break;
        default:
          res.status(400).send("unknown platform:" + platform);
          return;
      }
    }
    if (user) {
      const access_token = await this.authService.login(user);
      res.cookie(COOKIE_NAME, access_token, {
        httpOnly: true,
        sameSite: "none",
        secure: true,
        maxAge: 1000 * 60 * 60 * 24 * 7,
      });
      if (backUrl.startsWith("/")) {
        backUrl = `${process.env.DASHBOARD_URL}${backUrl}`;
      }
      res.redirect(302, backUrl);
      return;
    }
    res.status(500).send("Something went wrong.");
  }
}
分享到: