(Facebook) Authentication with Passport
此篇以 facebook 為例
前置作業
在串接 passport-facebook 之前, 需要先去 Facebook for Developers 建立應用程式, 並建立 Facebook 登入相關設定, clientID
與 clientSecret
即為應用程式編號與密鑰, 並且在登入設定那邊設定對應的 callback url
, 如圖所示, 或可參考 Facebook 文件
流程部分同 Passport-Google 那篇
建立 Guard 及 Strategy
先安裝所需套件
pnpm add passport-facebook --filter PACKAGE_NAME
pnpm add -D @types/passport-facebook --filter PACKAGE_NAME
建立 Guard
facebook.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { PassportUtility } from '@/core/guards/passport/utils/passport.utility';
@Injectable()
export class FacebookOauthGuard extends AuthGuard('facebook') {
constructor(private readonly passportUtility: PassportUtility) {
super();
}
async canActivate(context: ExecutionContext) {
const result = (await super.canActivate(context)) as boolean;
const request = context.switchToHttp().getRequest();
await super.logIn(request);
return result;
}
async getAuthenticateOptions(context: ExecutionContext) {
// you can get access to the request object.
const req = context.switchToHttp().getRequest();
return req.query;
}
handleRequest(error: any, user: any, info: any, context: ExecutionContext) {
return this.passportUtility.handleGuardRequest(error, user, info, context);
}
}
passport.utility.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Response } from 'express';
import { isEmpty } from 'lodash';
@Injectable()
export class PassportUtility {
constructor(private readonly configService: ConfigService) {}
public handleGuardRequest(error: any, user: any, info: any, context: ExecutionContext) {
if (error || !isEmpty(info)) {
const req = context.switchToHttp().getRequest();
const res: Response = context.switchToHttp().getResponse<Response>();
const originalUrl = `${this.configService.get<string>('general.selfUrl')}/signin`;
const queryString = req.originalUrl.split('?')[1] || '';
const redirectUrl = queryString !== '' ? `${originalUrl}?${queryString}` : originalUrl;
if (error) {
return res.redirect(`${redirectUrl}&error=${encodeURIComponent(JSON.stringify({ message: error.response.message }))}`);
}
return res.redirect(`${redirectUrl}&error=${encodeURIComponent(JSON.stringify({ message: info }))}`);
}
return user;
}
}
與 passport-local 相同的部分可以參考 passport-local
其他部分使用方式與 passport-google 相同
handleRequest
邏輯也是一樣的, 所以我們可以抽出來成一個 util function, 讓每個第三方認證的 handleRequest 都走相同的處理方式
建立 Strategy
facebook.strategy.ts
// ...
type FacebookJson = {
id: string;
email: string | undefined;
last_name: string | undefined;
first_name: string | undefined;
picture: {
data: {
url: string | undefined;
};
};
};
@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
constructor(
private readonly cryptUtility: CryptUtility,
private readonly configService: ConfigService,
private readonly passportService: PassportService,
private readonly userService: UserService
) {
super({
clientID: configService.get<string>('auth.authFacebookClientID'),
clientSecret: configService.get<string>('auth.authFacebookClientSecret'),
callbackURL: configService.get<string>('auth.authFacebookCallbackURL'),
scope: 'email',
profileFields: ['id', 'emails', 'name', 'picture', 'birthday', 'gender'],
passReqToCallback: true
});
}
async validate(req: ExpressRequest, _accessToken: string, _refreshToken: string, profile: any, done: VerifyCallback): Promise<any> {
const { id } = profile;
const _json: FacebookJson = profile._json;
let user: null | Partial<UserDocument>;
// do something with the profile
done(null, user);
}
}
Facebook 登入中預設能取得的使用者資訊有限, 這邊如果想要取得其他進階資料 ex: email
, 需要在應用程式中查看應用程式審查的權限與功能, 裡面會列出可以直接取得的權限, 以及需要經過 Facebook 審查通過後才能取得的權限
設定 Controller
auth.controller.ts
// ...
@Controller('auth')
export class AuthController {
constructor() // ...
{}
// ...
@UseGuards(FacebookOauthGuard)
@Get('facebook')
facebookSign() {}
@UseGuards(FacebookOauthGuard)
@Get('facebook/callback')
async facebookAuthCallback(@Request() req: ExpressRequest, @Response({ passthrough: true }) res: ExpressResponse) {
const url = await this.parseUtility.parseStateFromThirdPartyOauth(req.query.state as string);
// ...
url.search = new URLSearchParams([...Array.from(url.searchParams.entries()), ['st', req.session.uniqueId.toString()]]).toString();
res.redirect(`${url}`);
}
}
將 Strategy 加入 Module
cas.module.ts
// ...
@Module({
imports: [
// ...
PassportModule.register({ session: true })
],
providers: [
// ...
FacebookStrategy,
GoogleStrategy,
SessionSerializer
],
exports: [AuthService, UserService],
controllers: [AuthController, UserController]
})
export class CasModule {}
其餘皆與 passport-google 相同