本文章同時發佈於:
大家好,繼上一篇介紹完OAuth2.0 & OIDC後這次要介紹的就是如何實作Line Login
與NestJS
整合的部分。
在與NestJS
整合之前,我們必須先到Line Console上面創建Providers
,並創建Line Channel
,以獲得Channel ID, secret等等資訊。
NestJS
時會使用到ngrok是可以再開發的時候讓你免費調用HTTPS的工具
我們進入ngrok的網頁註冊登入
下載ngrok,並透過ngrok執行授權指令,最後執行./ngrok http 3000
複製ngrok
URL,並貼至Line Console的Callback URL
整體程式碼在此,大家可以Clone起來Run會比較好理解文章。
NestJS將MVC的分層切得很清楚,我們可以先看看最後的目錄架構:
所以MVC整體的概念我們可以以官網的這張圖來解釋:
有了MVC的整體概念後,我們可以借用上一篇OIDC的流程來說明有哪些路由,我們總共有兩個分別是:
code
的代碼至line/auth
,而line/auth
的controller
會透過此code
發Post至Line API來取得access token與OIDC的ID Token。我把需要設定的Config都集中放到.env.example
裡了,請把剛剛獲得的資訊都貼至此檔案,並將此檔案命名為.env
LINE_API_URI=https://api.line.me
LINE_ACCESS_URI=https://access.line.me
LINE_CLIENT_ID=<剛剛在Line Console獲得的Line Channel ID>
LINE_CLICLIENT_SECRET=<剛剛在Line Console獲得的Line Channel Secret>
SERVER_URI=<ngrok的URL,記得不要把路由/login/auth也貼上來了>
安裝並Run Server
$ npm install
$ npm run start:dev
在瀏覽器上瀏覽<ngrok的URL>/login
輸入自己的帳號密碼
這邊會詢問是否同意個人檔案
與用戶識別資訊
被讀取,點選許可即可,如果需要更多的資源,就需要進入Console調整,目前只有個人檔案的access token與用來識別的OIDC而已。許可之後Line 認證網站就會開始驗證。
驗證成功後就會將以下資訊帶往前端,前端會再將這些資訊帶至後端
// 後端必須用此code來去Line API拿取access token與OIDC ID Token
"code": "0ajVvTdwHLV4wQR2eFPM"
// 防止XSS攻擊的隨機亂碼
"state": "3e2e819f603adcfc6cb3f1761293efb591af206733e149351b2d19f43d9cf9c9a78a8a746449b763e471a9"
後端透過code參數發送Post至Line API,拿取access token
與OIDC ID Token
,這邊我有將資訊顯示在前端,不過要怎麼使用OIDC ID Token
完全取決你怎麼設計Server。
{
// 可以用來取的使用者檔案的access token
"access_token": "eyJhbGciOiJIUzI1NiJ9.ohEOAni8Y89mKrUGy1hrVl6oPmwsG5mcIQ1lOfAezzgy_s5tH-NNjWysXP8NEVEwlJwTR8V3XyiZsakzfd__HNdBGUK9um2AyqFroYzTSklGuEhFx3DtbDBKwZdAO1XmtSsZrcB90ka6dkqdK8BhYIIme6krvDDlsRPFV6Yg1QY.hto6BEx2C6mcDtcYQw43LK2kRpbm1mZ1fgM38xnoZFQ",
// access token的type
"token_type": "Bearer",
// 當access token過期時,可以拿此token去重新拿token
"refresh_token": "uRweZyH1XdgKhAoXnp2X",
// access token的期效
"expires_in": 2592000,
// 此次要求的資源
"scope": "openid profile",
// OIDC取得的ID Token
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY2Vzcy5saW5lLm1lIiwic3ViIjoiVTAxMDZkNWVhZDFhYzYyOTdmNDZmOTYxM2RhNmIzZDRiIiwiYXVkIjoiMTY1NDMxNDI3MSIsImV4cCI6MTU5MTU1MTkzNiwiaWF0IjoxNTkxNTQ4MzM2LCJub25jZSI6IjdlMzY1NDQ2M2NiNGY4ZGZiMzY1MTQxMzU2OGI2NDY2NmRmNDlkMWMyMjg3NWExODYyYzM0ZmU5MjliM2MyYzk0ZWVlNzQ3YjdiYzJhZjlmMTU1MzA5IiwiYW1yIjpbInB3ZCJdLCJuYW1lIjoi4ZWVKCDhkJsgKeGVl-aIkeS4jeaYr-WuuOWPsyDmiJHmmK_otoXntJrlkbHlkbEiLCJwaWN0dXJlIjoiaHR0cHM6Ly9wcm9maWxlLmxpbmUtc2Nkbi5uZXQvMGhpTFkxZTh3WE5tTmtTQjVqUmwxSk5GZ05PQTRUWmpBckhDMV9CQlpNT0ZZWmYzQTlXM2twQmhaQU9GTWRLM2sxRENrdVZVSkxhd1JQIn0.GVt5zG_QjiGZT3DfguGmz9jNX3hLwbPu4DxHHITHI5Y"
}
我們可以把ID Token貼到jwt.io解析參數,最重要的就是sub這個參數,這就像使用者的身分證字號,是唯一的,你可以拿此ID至你的Server帳戶系統整合。
{
// 發ID Token的Line API URL
"iss": "https://access.line.me",
// 關鍵的Line ID,可以拿此ID至你的Server帳戶系統整合
"sub": "U0106d5ead1ac6297f46f9613da6b3d4b",
// Line Channel ID
"aud": "1654314271",
// access token的過期的時間
"exp": 1591551936,
// access token生產的時間
"iat": 1591548336,
// 授權在URL中指定的值
"nonce": "7e3654463cb4f8dfb3651413568b64666df49d1c22875a1862c34fe929b3c2c94eee747b7bc2af9f155309",
// 使用者登入的方法,pwd只使用帳密登入
"amr": [
"pwd"
],
// 使用者名稱
"name": "ᕕ( ᐛ )ᕗ我不是宸右 我是超級呱呱",
// 使用者大頭貼
"picture": "https://profile.line-scdn.net/0hiLY1e8wXNmNkSB5jRl1JNFgNOA4TZjArHC1_BBZMOFYZf3A9W3kpBhZAOFMdK3k1DCkuVUJLawRP"
}
NestJS的CLI設計得相當完善,我們可以不用設定太多dirty thing就可以自動生產出一個NestJS Server。
首先我們安裝NestJS CLI,並創建project。
$ npm i -g @nestjs/cli
$ nest new project
安裝hbs
作為網頁渲染的引擎
$ npm install --save hbs
再main.ts
設定hbs
為網頁渲染引擎
// main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
);
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(3000);
}
bootstrap();
安裝Config模組
$ npm i --save @nestjs/config
透過CLI直接創建auth Controller
$ nest g controller auth
設定app.module.ts
,將Config、API、Auth Controller通通引入
// app.module.ts
import { Module, HttpModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthController } from './auth/auth.controller';
import { AuthService } from './auth/auth.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [HttpModule, ConfigModule.forRoot()],
controllers: [AppController, AuthController],
providers: [AppService, AuthService],
})
export class AppModule {}
Auth Controller講解,我註解在程式碼上
// auth.controller.ts
import { Get, Controller, Render, Query } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';
import * as qs from 'querystring';
import { AuthService } from './auth.service';
// login的前綴,也就是說,底下所有的Method的Route前面都會包含/login路徑
@Controller('login')
export class AuthController {
constructor(private authService: AuthService, private configService: ConfigService){
}
// /login的route,會生成帶有button的網頁回傳
@Get()
@Render('index')
root() {
// 亂數生成state
const state:string = crypto.randomBytes(43).toString('hex');
// 亂數生成nonce
const nonce:string = crypto.randomBytes(43).toString('hex');
// 將response_type, client_id, redirect_uri, state, scope, nonce組成button的query參數,傳至index.hbs渲染給瀏覽器
const query:string = qs.stringify({
response_type: 'code',
client_id: this.configService.get<string>('LINE_CLIENT_ID'),
redirect_uri: `${this.configService.get<string>('SERVER_URI')}/login/auth`,
state,
scope: 'profile openid',
nonce
})
return { lineAuthLoginURI: `${this.configService.get<string>('LINE_ACCESS_URI')}/oauth2/v2.1/authorize?${query}` };
}
@Get('/auth')
@Render('auth')
async auth(@Query('code') code) {
try {
// 拿到Line 認證網站的code之後,透過此code去Line API拿access token與OIDC ID Token
const token = await this.authService.postToken(code).toPromise()
return { token: JSON.stringify(token)}
} catch (err) {
console.log(err)
}
}
}
// auth.service.ts
// 引入HttpService模組,NestJS封裝的HTTP模組是透過Axios與RxJS整合的,擁有RxJS的方便的observable, observer, operator,可以組合各種非同步操作並利用operator管理資料流
import { Injectable, HttpService } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { map } from 'rxjs/operators';
import * as qs from 'querystring';
@Injectable()
export class AuthService {
constructor(private http: HttpService, private configService: ConfigService) {
}
postToken(code){
// 將code以Post的方式送市Line API,並且也要攜帶client_id, client_secret等等參數,另外redirect_uri必須與Line Console上設定的相同此request才會成功
return this.http.post(
`${this.configService.get<string>('LINE_API_URI')}/oauth2/v2.1/token`,
qs.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: `${this.configService.get<string>('SERVER_URI')}/login/auth`,
client_id: this.configService.get<string>('LINE_CLIENT_ID'),
client_secret: this.configService.get<string>('LINE_CLICLIENT_SECRET')
}),
{
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
})
.pipe(
map(response => response.data)
);
}
}
最後再把整體程式碼附上,怕大家有遺漏掉XD。
謝謝你的閱讀,也歡迎分享討論~最後再次附上家人為NestJS所畫的吉祥物圖XD。