iOS
TypeScript
Swift
cloudfunctions
Firestore

Firebase HTTPS callable function を試してみる

2018年3月20日の Firebase の更新で、クライアントから直接 Cloud Functions が叩ける機能が入りました。
Call Functions from Your App  |  Firebase

認証状態を持ったまま Cloud Functions を叩くことができるので、今までのように authenticated-json-apiauthorized-https-endpoint のような自前の認証は不要になりました。

使ってみる

iOS + Cloud Functions で使ってみました。

Cloud Functions

TypeScript の例です。

export const httpcallable = functions.https.onCall((data, context) => {
    if (!context.auth) {
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
            'while authenticated.')
    }

    if (!data.name) {
        console.log('data.name is not found')
        throw new functions.https.HttpsError('invalid-argument', 'data.name is undefined.', data)
    }

    return {
        uid: context.auth!.uid,
        name: data.name,
        data: data,
        context: context
    }
})

Cloud Functions は data, context を受け取ることができます。
data はクライアント側から渡すことのできるパラメータです。

context には認証情報などが含まれています、 context の型をみてみましょう。

export interface CallableContext {
    auth?: {
        uid: string;
        token: firebase.auth.DecodedIdToken;
    };
    instanceIdToken?: string;
}

認証済みだったら auth に値が入ってきます。auth があるか、anonymous かどうかもその内容で判定できます。
実際のパラメータはこのようになっていました。

未ログイン

context { instanceIdToken: 'hogehoge-token' }

anonymous ログイン

context { auth: 
   { uid: 'uid',
     token: 
      { iss: 'https://securetoken.google.com/myproj',
        provider_id: 'anonymous',
        aud: 'myproj',
        auth_time: 1521769673,
        user_id: 'uid',
        sub: 'uid',
        iat: 1521782508,
        exp: 1521786108,
        firebase: [Object],
        uid: 'uid' } },
  instanceIdToken: 'hogehoge-token' }

firebase のなかみ

firebase { identities: {}, sign_in_provider: 'anonymous' }

facebook ログイン

context { auth: 
   { uid: 'uid',
     token: 
      { iss: 'https://securetoken.google.com/myproj',
        name: 'Mike',
        picture: 'https://scontent.xx.fbcdn.net/v/jpg',
        aud: 'myproj',
        auth_time: 1521788688,
        user_id: 'uid',
        sub: 'uid',
        iat: 1521788688,
        exp: 1521792288,
        email: 'hoge@tfbnw.net',
        email_verified: false,
        firebase: [Object],
        uid: 'uid' } },
  instanceIdToken: 'hogehoge-token' }

firebase の中はこんな感じ

firebase { identities: 
   { 'facebook.com': [ '100258...' ],
     email: [ 'hoge@tfbnw.net' ] },
  sign_in_provider: 'facebook.com' }

注意

エラーを返す時は throw しましょう。 return new functions.https.HttpsError('invalid-argument', 'data.name is undefined.', data) だとクライアント側に無が返されます。 (result も error も nil が返る)

エラー

下記のエラー型が定義されています。HTTP Status Code のように使いやすく良いですね。

export declare type FunctionsErrorCode = 
  'ok' | 'cancelled' | 'unknown' | 'invalid-argument' | 
  'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 
  'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' |
  'data-loss' | 'unauthenticated';

iOS

これだけで認証済みで Functions を叩くことができます。便利ですね。

import FirebaseFunctions

functions.httpsCallable("httpcallable").call(["name": "taro"]) { (result, error) in
  if let error = error as NSError? {
    if error.domain == FunctionsErrorDomain {
      let code = FIRFunctionsErrorCode(rawValue: error.code)
      let message = error.localizedDescription
      let details = error.userInfo[FunctionsErrorDetailsKey]
    }
  }
  if let name = (result?.data as? [String: Any])?["name"] as? String {
    print(name)
  }
}

response は result or error になっています。

result.data に Functions が return したデータが入ってきます。
result.context に auth 情報も入っていました。

内部実装はどうなっているか

firebase-functions の内部実装を読んでみると、HTTP で叩いているだけのようです。
ざっくり読んだだけですが、 authorized-https-endpoint をラップしたメソッドを提供してくれているだけのようです、onCall のコードは 50 行程度なので読んでみると良いと思います。