46
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Firebase HostingでLINEログインする

Posted at

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ログインを組み込む

46
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?