Posted at

Firebase HostingでLINEログインする

More than 1 year has passed since last update.


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


シーケンス図

スクリーンショット 2017-12-19 18.20.47.png


ソースコード

全てのソースコードはこちらにあります。

https://github.com/kanemotos/firebase-login-sample


手順1.Firebase HostingのサイトをAngularで作成


terminal

# プロジェクト作成

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からリダイレクトされて戻って来る時の画面


app-routing.module.ts

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の設定


app.module.ts

@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」で指定しながら、移動します。


home.component.ts

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にログインできます。


line-auth-handler.component.ts

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的にこれで適切なのか自信がないです。サーバサイドに保存して照合すべきなのか、必要ないのか、誰か詳しい人いたらコメントお願いします。)


auth-state.service.ts

@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を設定します。


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',
},
},
};



  • こちらの手順を参考に、CloudFunctionsのURLを設定しておくのを忘れないで下さい。


  • こちらの手順を元に、LINEのチャネルIDを設定しておくのを忘れないで下さい。


手順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のパス設定や、サードパーティー製ライブラリの設定をします。


index.js

'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を発行してもらって、レスポンスします。


createCustomTokenForWeb.js

'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のコードをご参照ください。)


lineLoginUtil.js

'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のデプロイ


terminal

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ホスティングの起動


terminal

# ローカル起動

cd firebase-login-sample/line-login-app/
ng serve


参考

Google Developers Japan: LINE ログインによる Firebase ユーザーの認証

ウェブアプリにLINEログインを組み込む