HackMD
    • Sharing Link copied
    • /edit
    • View mode
      • Edit mode
      • View mode
      • Book mode
      • Slide mode
      Edit mode View mode Book mode Slide mode
    • Note Permission
    • Read
      • Only me
      • Signed-in users
      • Everyone
      Only me Signed-in users Everyone
    • Write
      • Only me
      • Signed-in users
      • Everyone
      Only me Signed-in users Everyone
    • More (Comment, Invitee)
    • Publishing
    • Commenting Enable
      Disabled Forbidden Owners Signed-in users Everyone
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Invitee
    • No invitee
    • Options
    • Versions
    • Transfer ownership
    • Delete this note
    • Template
    • Save as template
    • Insert from template
    • Export
    • Google Drive Export to Google Drive
    • Gist
    • Import
    • Google Drive Import from Google Drive
    • Gist
    • Clipboard
    • Download
    • Markdown
    • HTML
    • Raw HTML
Menu Sharing Help
Menu
Options
Versions Transfer ownership Delete this note
Export
Google Drive Export to Google Drive Gist
Import
Google Drive Import from Google Drive Gist Clipboard
Download
Markdown HTML Raw HTML
Back
Sharing
Sharing Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
More (Comment, Invitee)
Publishing
More (Comment, Invitee)
Commenting Enable
Disabled Forbidden Owners Signed-in users Everyone
Permission
Owners
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Invitee
No invitee
   owned this note    owned this note      
Published Linked with
Like BookmarkBookmarked
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
--- tags: cofacts --- [Rumors-api](https://github.com/cofacts/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](https://github.com/cofacts/rumors-api) is designed to support multiple *apps*, or "clients" in clinet/server model. The LINE bot server, [rumors-line-bot](https://github.com/cofacts/rumors-line-bot), is one of the app; [rumors-site](https://github.com/cofacts/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](https://g0v.hackmd.io/@mrorz/S1caurZq8), 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<br><small>(rumors-site)</small> | Backend apps<br><small>(rumors-line-bot)</small> | | --- | --- | --- | | [Identifying App](https://github.com/cofacts/rumors-api/blob/master/src/checkHeaders.js) | HTTP header `x-app-id` | HTTP header `x-app-secret` | | [Specifying user](https://github.com/cofacts/rumors-api/blob/master/src/index.js#L86-L88) | 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 | :::info 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 - Attach HTTP header `x-app-secret` with specified value - Specify desired `userId` denoting the current user performing the action 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](https://github.com/cofacts/rumors-api/blob/master/src/auth.js#L237-L239) (`/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](https://github.com/cofacts/rumors-api/blob/master/src/graphql/models/User.js#L103-L120) is designed so that: - If the object's `appId` is from a *browser app*, it resolves to the corresponding document in `users` Elasticsearch index. - If the object's `appId` is from a *backend app* and has the same `appId` currently accessing the API, it resolves to `{ id: <userId of the app> }`. - Otherwise, resolve to `null`. 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 `appId`s and their configuration in a centralized place, currently rumors-api directly hard-code the following `appId` throughout the codebase [[1]](https://github.com/cofacts/rumors-api/blob/master/src/checkHeaders.js), [[2]](https://github.com/cofacts/rumors-api/blob/master/src/auth.js#L189), [[3]](https://github.com/cofacts/rumors-api/blob/master/src/graphql/models/User.js#L110): - `RUMORS_LINE_BOT`: the **backend** app representing rumors-line-bot. Set when `x-app-secret` matches `process.env.RUMORS_LINE_BOT_SECRET`. - `BOT_LEGACY`: Not present in codebase, but presents in very old objects in the database, denoting that this data is collected by the legacy chatbot before Feb 2017. - `DEVELOPMENT_BACKEND`: the fallback **backend** app for wrong `x-app-secret`, used when - `WEBSITE`: - `DEVELOPMENT_FRONTEND`: 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 - [rumors-fb-bot](https://github.com/cofacts/rumors-fb-bot/) - [AI model](https://github.com/cofacts/rumors-ai) - [community builder](https://g0v.hackmd.io/@mrorz/B1X4EkJcU#New-idea-Social-media-toolkit) ### 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 的性質是: - **sensitive**: 可能含有 PII (email),因此若不是同一個 app,不該直接輸出 userId - **not safe**: 可能含有特殊字元,無法直接當成 Elasticsearch document `_id` - **long**: 長度可能會超過 [elasticsearch 上限 512 byte](https://blog.joshmlwood.com/elasticsearch-ids-are-hard/) 因此,設計上需要考慮: - 若 user ID from backend app 需要放進 Elasticsearch key 時應該要使用 `sha256(userId)` 而不能直接使用 `userId`,同時考慮到 sensitive、not-safe 與 long 這三個特性。 - 因為 sensitive 的關係,user Id from backend app 不應被其他 app 直接存取;但進行 query 的 app 可以存取自己寫入的 entity 的 userId (i.e. query 的 app id 若與 entity app Id 相同,才能拿出 user id),這樣此 app 才有機會將 rumors-api 的 entity 與自己的 user id 對在一起。 :::danger 雖說 backend app user ID 因為 *sensitive* 所以不該露出、因為 *not-safe* 且 *long* 所以不該用來拼接 entity ID,但事實上現在 production API 的某些欄位,並沒有遵循上面的原則,應當修正: ![](https://s3-ap-northeast-1.amazonaws.com/g0v-hackmd-images/uploads/upload_b0d42f7d823c5023e08e993e615b3bea.png) ::: --- 另外,API 在 Cofacts Next 時,會面對以下「指涉特定 backend user」的需求: - **display author of an entity**: 需要顯示特定 entity 的 user 資訊,如顯示 reply request 使用者的假名。此時 entity 帶有 `userId`、`appId`。 - **ensure entity uniqueness**: 有些 entity 組合,一個使用者只能有一個。如一個 user 只能對一個 article 送出一次 reply request。由於 Elasticsearch 沒有 unique key 機制,只能將 user ID、article ID 一併放在 - **user page**: 需要從 URL segment 讀出使用者 ID 然後顯示 user page。 - **fetch entities related to the user**: 「顯示此使用者的所有 reply request」、「列出此使用者所回報的文章列表」等等。 ### Implementation 下面兩種實作方向,均能滿足上列需求。 #### 方向 #1: backend 無實體 user 此解法在 elasticsearch 中,仍然需要一個 index 紀錄 backend user 的: - `_id: sha(appId + userId)` 確保同一個 appId / userId 組合不會產生兩個 doc - random generated name - random generated avatar 上面的 index 可以在 resolve user 時存取,若不存在則即時產生。 此實作的重點是在「`userId` 保密、又沒有相對應的 user entity」的限制下滿足「指稱 user」的需求。上述需求可以這樣實作: - **fetch entities related to the user**: query 時,用 `authorOf: {entityId, entityType}` 避開 userId,而是用「什麼東西的作者」來在 query 中指稱該使用者。API 會先在 resolver 中從資料庫找出相對應的 `userId` & `appId`,再使用 `userId` & `appId` 去撈相關 entity。 :::info 其實現在 `ListArticleFilter` 中的 [`fromUserOfArticleId`](https://github.com/cofacts/rumors-api/blob/master/src/graphql/queries/ListArticles.js#L98-L104) 就是一種「用別的 entity 指稱某個 LINE 使用者」的設計。 上面的 `authorOf` 就是將 `fromUserOfArticleId` 從指稱「單一 article 的 author」推廣到指稱「任何 entity 的 author」。 ::: - **user page**: 同理,URL 中放 `entityId`, `entityType`,在 API 中找出相對的 userId & appId - **display author of an entity**: user resolver 讀取 entity (如 replyrequests) 的 `appId` 與 `userId`,取得該 index 中儲存的 random name & avatar - **ensure entity uniqueness**: 將 `sha(appId + userId)` 放入 entity ID 以確保同一個使用者的限制。 :::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 分開處理,不如統一處理: - 兩者的 user 都一樣在 `users` index 下產生 user entity。 - 資料庫內所有 entity (`articles`, `articlereplies`, `replyrequests` 等等) 通通都使用 `users` 的 `_id`~~;刪除 `appId` 欄位。~~ - backend app 的 `user` entity ,與 browser app 的相比有下列特性: - `_id`: `sha256(appId + userId)` 確保 (appId, userId) 對應到唯一一個 user。(browser app user 的 `_id` 則維持由 Elasticsearch 自動生成) - 新增 `appUserId` 欄位,存放 backend app 送給 rumors-api 的原始 user Id。 `appUserId` 欄位,只有原 app 可存取(request 時 `x-app-secret` 須符合該 app)。 - 至此,有 *sensitive*、*long*、*not safe* 特性的東西被收進 `appUserId`,`users._id` 則是公開也沒關係的 `sha(appId + userId)`,因此可以與 browser app 的 user id 一樣外顯在 URL 裡,也可以編進 replyrequest、articlereplyfeedback 等 entity 的 `_id` 裡確保 uniqueness。 :::info `User` object type 要 expose `appUserId` 的話,需要檢查 `appId` 可以先不做,畢竟目前沒有 client 要讀 ::: - `name`, `avatarUrl`:[隨機產生](https://g0v.hackmd.io/7ERem43XREWJPOjjdE28SQ) - 新增 `appId` 欄位,存放是哪一個 backend app。(browser app 則無,畢竟所有 browser app 都共用由 rumors-api 統一管理的 user) - 新增 backend user 時機:[graphql server 取用 user 時](https://github.com/cofacts/rumors-api/blob/master/src/index.js#L79-L90)。流程如下: 1. 取 GraphQL call URL 中的 `userId` 與 `checkHeader` middleware 解出的 appId,~~產出 `sha(appId, userId)` 去 users index 撈出 user instance~~ term query 去撈該 (appId, appUserId) 的使用者 2. 若撈不到該 user,則就地產生一個 backend app user,插入 `users` DB (check collision) :::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 的邏輯一致: - **display author of an entity**: 拿 entity 的 `userId` 去 users index 撈出該 user 的資訊。行為與 browser app users 一致。 - **ensure entity uniqueness**: ~~直接把 entity 的 `userId` encode 進該 entity 的 ID 方可確保 uniqueness。~~ graphql context 層會 find or insert uesr,取用 `_id` 拿來放進 entity id 中 (如 replyrequest, articlereplyfeedback) - **user page**: URL 裡直接使用 users index 裡的 `_id` (`sha(appId + userId)`)。行為與 browser app users 一致。 - **fetch entities related to the user**: 直接使用 users index 裡的 `_id` (`sha(appId + userId)`) 去比對資料庫裡各 entity 的 `userId` 欄位。行為與 browser app users 的一致。

Import from clipboard

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lost their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.


Upgrade

All
  • All
  • Team
No template.

Create a template


Upgrade

Delete template

Do you really want to delete this template?

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

Sign in via GitHub

New to HackMD? Sign up

Help

  • English
  • 中文
  • 日本語

Documents

Tutorials

Book Mode Tutorial

Slide Example

YAML Metadata

Resources

Releases

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions

Versions

Upgrade now

Version named by    

More Less
  • Edit
  • Delete

Note content is identical to the latest version.
Compare with
    Choose a version
    No search result
    Version not found

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.