Skip to main content

(Facebook) Authentication with Passport

此篇以 facebook 為例


前置作業

在串接 passport-facebook 之前, 需要先去 Facebook for Developers 建立應用程式, 並建立 Facebook 登入相關設定, clientIDclientSecret 即為應用程式編號與密鑰, 並且在登入設定那邊設定對應的 callback url, 如圖所示, 或可參考 Facebook 文件

facebook-1.png

facebook-2.png

流程部分同 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 審查通過後才能取得的權限

facebook-3.png

設定 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 相同