特に難しくはないが、今後新規プロジェクトを始めるたびに考えずにコピペで済ませられるように記録に残しておく。
Angular2とFirebase hostingでSPA開発環境を整える
の続編のような感じ。
やりたいこと
- AngularDart(SPA)からFirebase Authenticationを行いたい
- Backend serverのAuthorizationを行いたい
- Authenticationが必要なページと必要ないページが混在できるようにしたい
サンプルコード
https://github.com/ntaoo/ng2_firebase_gcp/tree/master/authentication
Consoleを操作してAuthentication Providerを有効化する
SIGN-IN METHODでAuthenticationに使用するProvider有効化する。今回はGoogleのみにするが他のProviderでも特につまづかないはず。
Firebase Package
firebase packageを使用する。(firebase3 packageの作者の協力で、firebase3に対応したものが再びfirebase/firebase-dartリポジトリでメンテナンスされるようになっている)
firebase script tagをhtml>headに追加しておくこと。
FirebaseService
Firebase APIをAngularDartで利用したいので、ServiceにしてAppComponentのproviderに登録しておく。
import 'package:angular2/angular2.dart';
import 'package:firebase3/firebase.dart' as firebase;
@Injectable()
class FirebaseService {
final firebase.Auth auth;
factory FirebaseService() {
firebase.initializeApp(
apiKey: " AIzaSyCIQNJV-TFRl9BK_9hVuS8lkgMxD69_Z0A",
authDomain: "ng2-firebase-gcp.firebaseapp.com",
databaseURL: "https://ng2-firebase-gcp.firebaseio.com",
storageBucket: "ng2-firebase-gcp.appspot.com");
return new FirebaseService._(firebase.auth());
}
FirebaseService._(this.auth);
}
AuthService
FirebaseServiceに依存してこのSPAアプリのauthを担当するserviceも作成し、同じくAppComponentのproviderに登録しておく。
/// 前略
@Injectable()
class AuthService {
final firebase.Auth _auth;
final StreamController<bool> _onAuthStateChangedController;
/// Nullable. If null, it means that firebase authentication process has not been completed yet (intermediate state).
User user;
/// This getter doesn't care the intermediate state. Use [checkUserAuthenticated()] if that state is possibility.
bool get isUserAuthenticated => user is AuthenticatedUser;
Stream<bool> get onAuthStateChanged => _onAuthStateChangedController.stream;
AuthService(FirebaseService firebaseService)
: _auth = firebaseService.auth,
_onAuthStateChangedController = new StreamController<bool>.broadcast() {
_listenFirebaseOnAuthStateChanged();
}
/// 後略
Sign In
FirebaseのSign Inには、SignInWithPopup
とSignInWithRedirect
の2つのメソッドがある。
SignInWithPopup
はSPAアプリの同一ページのままConsent ScreenがPopupする制御フローとなり、SignInWithRedirect
はConsent Screenページに遷移したあと、元のSPAアプリのページにリダイレクトされる制御フローとなる。
SignInWithPopup
はページ遷移しないためSign Inフローの制御がしやすい。ただし、Mobile WebでのUXを考慮する場合はSignInWithRedirect
を選択するのが望ましい。
今回は両方のメソッドを実装し、サンプルコードを公開している。
SignInWithPopup
- FirebaseのAPI(
signInWithPopup
)をcallする。(_runBackendAuthorizationについては、後述の「Backend serverのAuthorization」を参照)
// Assuming only google auth on this example.
Future signInWithPopup() async {
var credential =
await _auth.signInWithPopup(new firebase.GoogleAuthProvider());
return _runBackendAuthorization(credential);
}
- AuthServiceのconstructor bodyでlistenしておいた、firebase.auth.onAuthStateChangedイベントを処理する。
void _listenFirebaseOnAuthStateChanged() {
_auth.onAuthStateChanged.listen((firebase.AuthEvent event) {
_handleOnAuthStateChanged(event);
_onAuthStateChangedController.add(isUserAuthenticated);
});
}
void _handleOnAuthStateChanged(firebase.AuthEvent event) {
var u = event.user;
user = u != null ? new AuthenticatedUser(u) : new GuestUser();
}
firebase.auth.onAuthStateChangedイベントは主にsign in, sign outで発火し、Sign Inすればevent.user
が存在し、そうでなければevent.user
はnullとなる。
- authentication成功後、クライアントサイドルーティングでページ遷移。
認証済みページ等に遷移させる。
Future<Null> signInWithPopup() async {
await _authService.signInWithPopup();
if (await _authService.checkUserAuthenticated()) _router.navigate(['Some']);
}
SignInWithRedirect
Mobile Webを考慮すればこちらのメソッドを選択するべきだが、いったんFirebaseによる認証画面に遷移し、SPAアプリにリダイレクトされる場合、SPAアプリがreloadされることになる。リダイレクト先URLを指定することはできないため、認証後のリダイレクトであることを判別するため、あらかじめその情報を一時的に保持しておく必要がある。
今回は、SessionStorageに保持しておくことにした。
Future<Null> signInWithRedirect() {
window.sessionStorage[_willRedirectKey] = 'true';
return _authService.signInWithRedirect();
}
Firebaseからリダイレクトされたとき、SessionStorageの情報を参照し、リダイレクト直後の起動であるかどうかを判定する。
Future ngOnInit() {
return new Future.sync(() async {
if (_redirectedFromFirebase()) {
await _authService.handleSignInWithRedirectResult();
window.sessionStorage.remove(_willRedirectKey);
if (await _authService.checkUserAuthenticated()) {
_router.navigate(['Some']);
}
}
});
}
firebase.auth.onAuthStateChangedイベントが発火することになり、SignInWithPopupの場合と同じく、認証処理が完了する。
この認証フローの場合のcredentialはFirebaseのgetRedirectResult() APIで取得する。
Future handleSignInWithRedirectResult() async {
var credential = await _auth.getRedirectResult();
return _runBackendAuthorization(credential);
}
プロダクションアプリではリダイレクト後の認証処理の間、全画面progress circleなどを表示しておくと良い。
Sign Out
FirebaseのsignOut() APIをcallするだけ。onAuthStateChangedイベントが発火する。
Backend serverのAuthorization
Firebaseだけではできない処理を行うために、別途Backend serverを用意し、Authorizationしたい。
そのためトークンをBackend serverに渡し、トークンをverifyさせる。
適宜server側のCORSの設定を行うこと。以下の例ではAuthorizationヘッダーをリクエストに追加しているので、serverにCORSでアクセスさせる場合は、'Access-Control-Allow-Headers'に'Authorization'を許可する設定が必要となる。
Future _runBackendAuthorization(firebase.UserCredential credential) async {
var idToken = await credential.user.getToken();
var client = new http.BrowserClient();
try {
return await client.post('http://localhost:9920/authorization',
headers: {'Authorization': idToken});
} catch (e) {
print("Error on '_runBackendAuthorization': $e");
}
}
サーバー側の実装は省略。
いくつかの言語ではverify_firebase_token()
的なAPIを叩けるライブラリすら用意されている。
tokenをverifyし、userIdやexpired time等を取得し、それらを含めた必要な情報を永続化してキャッシュし、その後は必要に応じてまたtokenをverifyすれば良い。
クライアントSPAは、有効なtokenを毎回Authorization headerにセットしてhttpRequestする。
ユーザー認証済みでなければアクセスできないページを設定する
特定のページを、ユーザー認証済みでなければアクセスできないようにしたい。
AngularDartのrouterにはCanActivate
APIがあるため、これをアノテーションにしてページを表すComponentに付加し、認証済みであるかどうかを判定するメソッドを呼び出す。
@Component(
selector: 'some',
template: 'Some auth required component. user is {{authService.user.displayName}}')
@CanActivate(checkAuthenticated)
class SomeComponent {
AuthService authService;
SomeComponent(this.authService);
}
ただし、APIの制限によりAuthServiceのcheckUserAuthenticated()を直接呼び出すことができないため、ワークアラウンドコードを書く。
あらかじめAppComponentのコンストラクタでsetInjector()
を実行してInjectorをセットしておき、checkAuthenticated()内でそのInjectorからAuthServiceを取得し、checkUserAuthenticated()
で認証済みであるかを判定する。
import 'dart:async';
import 'package:angular2/angular2.dart';
import 'package:angular2/router.dart';
import 'model/service/auth_service.dart';
Injector injector;
void setInjector(Injector _injector) {
injector = _injector;
}
Future<bool> checkAuthenticated(
ComponentInstruction _, ComponentInstruction __) async {
bool isAuthenticated =
await injector.get(AuthService).checkUserAuthenticated();
if (!isAuthenticated) injector.get(Router).navigate(['SignIn']);
return isAuthenticated;
}
Router APIは近々書きかえられるようなので、このような不格好なワークアラウンドコードを書かなくてもよくなるかもしれない。
あとがき
もしももっと良い方法があれば教えてください。