Overview
Firebase Hostingを使って、LINEログインさせるWEBページを作りたかったのですが、下記のサイトではLINE APIの仕様が古かったり(v1)で、順調には行かなかったので、LINEログインv2.1版として書いてみました。
Google Developers Japan: LINE ログインによる Firebase ユーザーの認証
前提条件
利用するサービスやAPIバージョン
- Firebase Hosting (AngularFire2を利用)
- Firebase functions (外部のLINE APIへ通信するのでSparkプラン以外)
- LINEログインv2.1
シーケンス図
ソースコード
全てのソースコードはこちらにあります。
https://github.com/kanemotos/firebase-login-sample
手順1.Firebase HostingのサイトをAngularで作成
# プロジェクト作成
mkdir firebase-login-sample
cd firebase-login-sample/
ng new line-login-app --skipInstall
cd line-login-app
npm-check-updates -u --removeRange
npm install
npm install --save typescript@2.4.2 firebase angularfire2
# ルーティング、コンポーネント、サービス、インタフェース作成
ng generate module app-routing --flat --module=app
ng generate component components/home
ng generate component components/line-auth-handler
ng generate service services/auth-state
ng generate interface interfaces/create-custom-token-response
ルーティングの設定
ポイントは、下記のように2つの画面を用意する点です。
URL | Component | 説明 |
---|---|---|
example.com/ | HomeComponent | ログインボタンを置く画面 |
example.com/line_auth_handler | LineAuthHandlerComponent | LINEからリダイレクトされて戻って来る時の画面 |
const routes: Routes = [
{path: '', component: HomeComponent, pathMatch: 'full'},
{path: 'line_auth_handler', component: LineAuthHandlerComponent},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {
}
##AppComponentの設定
@NgModule({
declarations: [
AppComponent,
HomeComponent,
LineAuthHandlerComponent,
],
imports: [
BrowserModule,
HttpClientModule, // MUST include this under 'imports' after BrowserModule.
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent],
})
Home Component
Lineログインボタンが押されたあとの動作を書きます。
LINE認証が終わったあとに戻ってくる画面のURLを「redirect_uri」で指定しながら、移動します。
export class HomeComponent implements OnInit {
constructor(public afAuth: AngularFireAuth,
private authStateService: AuthStateService) {
}
ngOnInit() {
}
loginWithLine() {
const response_type = encodeURIComponent('code');
const client_id = environment.line.login.channel_id;
const redirect_uri = encodeURIComponent(window.location.href + 'line_auth_handler');
const state = this.authStateService.getState();
const scope = encodeURIComponent('openid profile');
const url = `https://access.line.me/oauth2/v2.1/authorize?response_type=${response_type}&client_id=${client_id}&`
+ `redirect_uri=${redirect_uri}&state=${state}&scope=${scope}`;
window.location.href = url;
}
logout() {
this.afAuth.auth.signOut();
}
}
LineAuthHandlerComponent
LINEからリダイレクトされて戻ってきた際の処理をします。
「code」というリクエストパラメータが渡されるので、それをFirebase Functionsで作るAPIに渡します。
APIでは、FirebaseのCustomTokenを作ってもらえるようにコーディングします。(詳細は下部参照)
なので、CustomTokenが返却されたら、これを元にsign-inすればFirebaseにログインできます。
export class LineAuthHandlerComponent implements OnInit {
constructor(private authStateService: AuthStateService,
private route: ActivatedRoute,
private router: Router,
private http: HttpClient,
private afAuth: AngularFireAuth) {
this.route.queryParams.subscribe((params: Params) => {
const responded_error = params['error'];
const responded_error_description = params['error_description'];
if (responded_error) {
console.error(responded_error_description);
this.router.navigate(['/']);
return;
}
const code = params['code'];
const state = params['state'];
if (this.authStateService.checkState(state) === false) {
console.error('state is wrong');
this.router.navigate(['/']);
return;
}
Promise.resolve().then(() => {
// Request to Cloud Functions
const url = `${environment.cloud_functions.host_name}/api/auth/line_login/web/custom_token`;
const redirectUri = (window.location.href).replace(/\?.*$/, '');
const body = {
code: code,
redirect_uri: redirectUri,
};
return this.http.post<CreateCustomTokenResponse>(url, body, {
headers: new HttpHeaders()
.set('Content-Type', 'application/json')
,
}).toPromise();
}).then((response: CreateCustomTokenResponse) => {
return this.afAuth.auth.signInWithCustomToken(response.firebase_token);
}).then(() => {
console.log('signed in successfully.');
this.router.navigate(['/']);
}).catch((error) => {
console.error('failed to sign in.');
console.error(error);
});
},
);
}
ngOnInit() {
}
}
AuthStateService
Open IDの「state」をフロントエンドでランダム文字列(UUID)にして、リダイレクトされたフロントエンドで照合するためのサービスです。
(しかし、Open ID connect的にこれで適切なのか自信がないです。サーバサイドに保存して照合すべきなのか、必要ないのか、誰か詳しい人いたらコメントお願いします。)
@Injectable()
export class AuthStateService {
constructor(private cookieService: CookieService) {
}
getState() {
const state = UUID.UUID().toString();
this.cookieService.set('auth_state', state);
return state;
}
checkState(target: string): boolean {
const state = this.cookieService.get('auth_state');
const checkResult = (state === target);
this.cookieService.delete('auth_state');
return checkResult;
}
}
環境変数の設定
こちらの手順を元に、environment.tsを設定します。
export const environment = {
production: false,
firebase: {
apiKey: 'XXXXX',
authDomain: 'XXXXX',
databaseURL: 'XXXXX',
projectId: 'XXXXX',
storageBucket: 'XXXXX',
messagingSenderId: 'XXXXX',
},
cloud_functions: {
host_name: 'https://XXXXX.cloudfunctions.net',
},
line: {
login: {
channel_id: 'XXXXXXXXXX',
},
},
};
手順2.Firebase Functionsを作成
Firebase Functionsでは、LINEから返却される「code」を元に、LINEアクセストークンを取得し、LINEのUserIDを取得し、FirebaseのCustomTokenを発行するAPIを作成します。
# プロジェクト作成
cd firebase-login-sample/
mkdir line-login-functions
cd line-login-functions
firebase init functions
cd functions
npm-check-updates -u --removeRange
npm install
npm install --save body-parser express express-validator cookie-parser cors request request-promise
# functions config 設定
furebase use --add
firebase functions:config:set service_account="$(cat service-account.json)"
firebase functions:config:set line.login.channel_id=XXXXXXXXXX
firebase functions:config:set line.login.client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- こちらの手順を元にFirebaseのservice-accountを取得し、上記のコマンドでfunctions configを設定しておくのを忘れないで下さい。
- こちらの手順を元に、LINEのチャネルIDとチャネルシークレットを取得し、上記のコマンドでfunctions configを設定しておくのを忘れないで下さい。
index.js
APIのパス設定や、サードパーティー製ライブラリの設定をします。
'use strict';
// Modules imports
const functions = require('firebase-functions');
const rp = require('request-promise');
// Firebase Setup
const admin = require('firebase-admin');
const serviceAccount = functions.config().service_account;
if (!serviceAccount) {
throw new Error('please set SERVICE ACCOUNT before you deploy!');
} // you can remove this if statement after you set.
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const bodyParser = require('body-parser');
const express = require('express');
const expressValidator = require("express-validator");
const cookieParser = require('cookie-parser')();
const cors = require('cors')({origin: true});
const app = express();
app.use(bodyParser.json());
app.use(expressValidator([])); // this line must be immediately after any of the bodyParser middlewares!
app.use(cors);
app.use(cookieParser);
app.post('/auth/line_login/web/custom_token', require('./api/auth/line_login/createCustomTokenForWeb').handler);
// Expose the API as a function
exports.api = functions.https.onRequest(app);
createCustomTokenForWeb.js
codeを受け取ってLINEのアクセストークンを取得する処理です。
lineLoginUtilにアクセストークンを渡して、CustomTokenを発行してもらって、レスポンスします。
'use strict';
const functions = require('firebase-functions');
const rp = require('request-promise');
// Line Setup
if (!functions.config().line ||
!functions.config().line.login ||
!functions.config().line.login.channel_id ||
!functions.config().line.login.client_secret) {
throw new Error('please set LINE login CHANNEL_ID & CLIENT_SECRET before you deploy!');
}
const line_login_channel_id = functions.config().line.login.channel_id;
const line_login_client_secret = functions.config().line.login.client_secret;
// API code
exports.handler = ((req, res) => {
req.checkBody('code').notEmpty();
req.checkBody('redirect_uri').notEmpty();
const code = req.body.code;
const redirect_uri = req.body.redirect_uri;
req.getValidationResult().then(result => {
console.info('validating requested data.');
if (!result.isEmpty()) {
return res.status(400).json({
error: result.array()
});
}
}).then((error) => {
if (error) {
console.error(error);
throw new Error('validating request Error');
}
console.info('validated. (result OK)');
return Promise.resolve();
}).then(() => {
const options = {
method: 'POST',
uri: 'https://api.line.me/oauth2/v2.1/token',
json: true,
timeout: 10 * 1000,
form: {
grant_type: 'authorization_code',
code: code,
redirect_uri: redirect_uri,
client_id: line_login_channel_id,
client_secret: line_login_client_secret,
},
};
return rp(options);
}).then((response) => {
console.log('requested token from code successfully.');
return Promise.resolve(response);
}).then((response) => {
const lineAccessToken = response.access_token;
const line_login_util = require('./lineLoginUtil');
return line_login_util.createFirebaseCustomToken(lineAccessToken);
}).then((firebaseCustomToken) => {
const result = {
firebase_token: firebaseCustomToken,
};
return res.status(200).json(result);
}).catch(error => {
console.error('Error: ', error);
return res.status(500).json({error: error});
});
});
lineLoginUtil.js
LINEアクセストークンを元に、LINEプロフィールAPIを呼び出します。
本記事では触れていませんが、iOS/Android用のcreateCustomTokenForClient.jsと処理を共通化するために、ファイルを分けて書いています。(詳細は、Githubのコードをご参照ください。)
'use strict';
const admin = require('firebase-admin');
const rp = require('request-promise');
exports.createFirebaseCustomToken = function (lineAccessToken) {
let firebaseUid;
return Promise.resolve().then(() => {
const options = {
method: 'GET',
url: 'https://api.line.me/v2/profile',
json: true,
timeout: 10 * 1000,
headers: {
'Authorization': `Bearer ${lineAccessToken}`,
},
};
return rp(options);
}).then((response) => {
console.log('requested profile successfully.');
return Promise.resolve(response);
}).then((response) => {
if (!response.userId) {
return Promise.reject('No userId.');
}
firebaseUid = 'line:' + response.userId;
return admin.auth().getUser(firebaseUid).then(() => {
console.log(`user ${firebaseUid} was found.`);
}).catch((error) => {
if (error.code === 'auth/user-not-found') {
const createRequest = {
uid: firebaseUid,
};
if (response.displayName) createRequest.displayName = response.displayName;
if (response.pictureUrl) createRequest.pictureUrl = response.pictureUrl;
return admin.auth().createUser(createRequest).then(() => {
console.log('created user successfully.');
return Promise.resolve();
});
} else {
return Promise.reject(error);
}
});
}).then(() => {
const claims = {
provider: 'LINE',
};
return admin.auth().setCustomUserClaims(firebaseUid, claims);
}).then(() => {
console.log('set custom user claims successfully.');
return Promise.resolve();
}).then(() => {
return admin.auth().createCustomToken(firebaseUid);
}).then((firebaseCustomToken) => {
console.log('created custom token successfully.');
return Promise.resolve(firebaseCustomToken);
});
};
CloudFunctionsのデプロイ
cd firebase-login-sample/line-login-functions/functions/
firebase deploy --only functions:api
手順3.LINEのコールバックURLの設定
こちらの手順を元に、LINEコンソールでコールバックURLを設定して下さい。
(設定例)
http://localhost:4200/line_auth_handler
https://XXXXX.firebaseapp.com/line_auth_handler
Firebaseホスティングの起動
# ローカル起動
cd firebase-login-sample/line-login-app/
ng serve
参考
Google Developers Japan: LINE ログインによる Firebase ユーザーの認証
ウェブアプリにLINEログインを組み込む