Rumors-api userId & appId management proposal

This document explains the machanism of userId & appId in rumors-api codebase, and proposes a change to the status quo.

[TOC]

Background

rumors-api is designed to support multiple apps, or “clients” in clinet/server model. The LINE bot server, rumors-line-bot, is one of the app; rumors-site, the reply-editing application, is another.

When different apps invoke mutative rumors-api APIs to create object entities (articles, replies, feedbacks, etc), the API records the corresponding appId and the userId provided by the app to the created entities. In the database schema, you can see that almost every index has userId and appId fields; they can be also seen in nested objects as well, such as replyrequests.feedbacks, articles.articleReplies, etc.

Identifying the app and user

rumors-api provides 2 ways for apps to identify themselves and the current user when making requests to its GraphQL API:

App type Browser apps
(rumors-site)
Backend apps
(rumors-line-bot)
Identifying App HTTP header x-app-id HTTP header x-app-secret
Specifying user Login cookie in cofacts-api.g0v.tw domain, managed by rumors-api ?userId=xxx query param
Forgery protection CORS origin & appId mapping Shared secret between the app and rumors-api

[!NOTE] Lucien: 直接用 domain 就好,這樣應該就不用 x-app-id

orz 對耶

Lucien 啊你是 for CSRF 嗎

orz 喔對齁。對對。

Lucien 了解,這樣確實要 HTTP header。

For Backend Apps

When backend apps (like rumors-line-bot) talk to rumors-api, it should

The x-app-secret acts like an secret API key. If the given x-app-secret maps any known app secret, rumors-api will set appId corresponding to the secret, and take whatever userId the backend app provides in /api/graphql URL path.

For Browser Apps

When browser apps (like client-side render part of rumors-site) talk to rumors-api, it should provide x-app-id HTTP header.

When receiving OPTION request with x-app-id, rumors-api will set access-control-allow-origin to the domain corresponding to that appId, allowing subsequent cross-origin AJAX calls from the browser app.

Security aspect: is it possible to forge appId?

Although rumors-api takes in the public, unencrypted x-app-id directly from the client side, appId forgery can still be prevented by combining HTTP header x-app-id and CORS origin together. The HTTP header limits the attacker to use AJAX, and the CORS origin blocks the attack from any domain other than the one that corresponds to the appId.

Authentication of Browser Apps

On the contrary to backend apps, userId of browser apps are managed by rumors-api. This is designed so that browser apps don’t need to implement their own authentication.

Rumors-api expect browser apps redirect the user to one of its login endpoints (/login/facebook, /login/twitter, /login/github), finish OAuth login flow, and redirect back to the browser app. After that, whenever the browser app makes request to rumors-api, it uses the login session to determine the userId of current API call. The login session cookie is managed by rumors-api itself.

Resolving user in user field

Many object type from rumors-api, including Article, ArticleReply, ArticleReplyFeedback, has a user field indicating the author of that object.

The user field resolver is designed so that:

This ensures that

  1. Users managed by rumors-api (i.e. users used by browser apps) can be accessed openly by any app (even when no appId is specified at all), since they are physically stored in the database under our control, and we leak no personal data in users’ ID and any public fields.
  2. For user managed by backend apps, rumors-api has nothing but the user ID stored on the object. Since each backend app manages their own userId, we want to prevent userId from one backend app leaks to other apps, just in cast that any backend app uses personal data (such as plaintext email) directly as user ID. We still expose userId to the backend app that created the object, so that the backend app can still bridge between the objects in rumors-api database and the owner entities in the app’s own user database.

Current implementation

Instead of managing all appIds and their configuration in a centralized place, currently rumors-api directly hard-code the following appId throughout the codebase [1], [2], [3]:

These code are introduced into the codebase for faster development when we kick-start the product. They are subject to change, as we are currently facing the following challenges.

Challenges we are facing now

Emerging clients

Naming backend app users

https://g0v.hackmd.io/KVzYgGsARZuyd3AJjJtxlA#%E5%81%87%E5%90%8D%E8%A8%8E%E8%AB%96

Proposed implementation to overcome the challenges

storage specify dev-only secret (.env.sample), only available on staging DB block all other usage


Representation of backend app users

I.e. rumors-line-bot 的使用者在 rumors-api 如何表示。

Backend app user 的需求

User IDs from backend app 的性質是:

因此,設計上需要考慮:

[!CAUTION] 雖說 backend app user ID 因為 sensitive 所以不該露出、因為 not-safe 且 long 所以不該用來拼接 entity ID,但事實上現在 production API 的某些欄位,並沒有遵循上面的原則,應當修正:


另外,API 在 Cofacts Next 時,會面對以下「指涉特定 backend user」的需求:

Implementation

下面兩種實作方向,均能滿足上列需求。

方向 #1: backend 無實體 user

此解法在 elasticsearch 中,仍然需要一個 index 紀錄 backend user 的:

上面的 index 可以在 resolve user 時存取,若不存在則即時產生。

此實作的重點是在「userId 保密、又沒有相對應的 user entity」的限制下滿足「指稱 user」的需求。上述需求可以這樣實作:

[!WARNING] 需要 migration script

  • 重新生成 _id 裡有原始 app userId 的 entity:所有 replyrequest、articlereplyfeedback 的 _id 裡,appId + userId 的部分都要改用 sha(appId + userId) 重新生成。

方向 #2: 針對每個 backend user 都產生一個 user document

與其把 backend app user 與 browser app user 分開處理,不如統一處理:

[!WARNING] 需要 migration script

  • 把現在所有 entity (articles, replyrequests, articlereplyfeedbacks 等等) 裡的 user 都蓋出 user instance
  • 把所有 entity 的 userId 都從 backend app 給的原始 userId,更新成新的 ID (從資料庫撈)
  • 重新生成 _id 裡有原始 app userId 的 entity:所有 replyrequest、articlereplyfeedback 的 _id 裡,appId + userId 的部分都要改用 sha(appId + userId) 重新生成。

需要變更 DB schema

  • users 新增上面提到的欄位
  • 各 entity 移除 appId 欄位。 Lucien: 還是保留 appId 紀錄這個 entity 是哪個 app 做的,方便用來做 filter。

如此一來,上面列出的需求的實作就會變得非常單純,且與指涉 browser app user 的邏輯一致: