# Authentication & Authorization
###### tags: `HTTP` `Cookie` `Session` `JWT` `OAuth` `PassportJS`
## [參考文章(認證系統)](https://vicxu.medium.com/authentication-那些小事上集-cookie-與-session-介紹-1da2d413afa2)
## 授權與認證
許多 Web application 都有會員系統的功能,在開發會員系統的過程中,使用者的身份認證是重要功能之一,用戶身份認證可說是系統資安的第一道防線,如果身份認證的過程中有任何缺失,會導致會員資料外洩,以及會員授權等問題。
## 瀏覽器儲存資料的地方
### Storage
* LocalStorage
* SessionStorage
* IndexedDB
* Web SQL
* Cookies
### Cache
* Cache
## Cookies
### 介紹與使用場景
* 是一個由瀏覽器(Browser)有關的資料儲存技術,儲存在client端硬碟中(不會揮發)
* 瀏覽器開始運作時,資料會被載入到記憶體中(資料一多佔資源)
* 以Chrome舉例,儲存在SQLite的輕型文字資料庫中管理
* 通常儲存資料容量小且無意義的資訊
* 屬於Stateful的設計模式,不適合API-based的設計
### 運作方式
* 描述client 與 server之間的溝通狀態,使用name-value(dict)的形式紀錄
* 由於Http是 stateless的設計,需要兩端同步狀態,所以在Http使用 Cookies & Set-Cookies 兩個Header欄位紀錄與更新Cookies
* ***同源政策***: Server要求讀取 Client的Cookies,需要滿足被記錄的協議>網域>端口,以防止用戶資料洩漏到不同網站

#### Hint: 除了Cookies之外,請求LocalStorage、DOM、Ajax請求也需要滿足同源政策
## Session
### 介紹與使用場景
* 目的也是紀錄 cli-ser之間的狀態,差別是Cookies儲存在client(e.g Browser),Session儲存在server(e.g Redies)
* 儲存購物車資料、用戶資料、信用卡訊息
* 一樣屬於Stateful的設計模式,不適合API-based的設計
* 用戶一多,可能會造成Server端的儲存空間、快取空間負載過重
### 運作方式
* 彌補Cookies資料量限制,把大多主要資料儲存在Server,安全又省流量
* 使用Session Key代替傳送原本全部Cookies的資訊
* 通過使用者驗證後,Server回傳一組Session Key,因為是一組編碼,client端無法知道其中結構,確保安全性。

#### Hint: Session-based authentication 是 stateful 的驗證機制,也就是 Server 端和 Client 端都必須儲存狀態資訊,例如 Server 端必須將使用者資料存在 Session database,而Client 端也必須用 Cookie 儲存 session id。
## JWT
### [防止JWT被攻擊](https://auth0.com/blog/a-look-at-the-latest-draft-for-jwt-bcp/)
### 介紹與使用場景
* 將一組敏感的資訊,轉換成一組無意義的資訊
* Token 主要的用途驗證權限。
* Token 驗證系統要確保 Token 來源,假設沒有這項措施,就有可能使用A網站的Token進入B網站
* 驗證 Token 來源使用雜湊函數 (Hash),常見的有 SHA-1、SHA-2、 SHA-256
* Hash被使用於驗證資料的完整性(i.e 資料是否被竄改過),一旦資料被竄改雜湊值也會更改,當作資料的完整性簽章 (Signature)。
* Token 儲存在 Client-side (i.e LocalStorage, SessionStorage, Cookies)。
* 優點:Token 可以作為 JSON data 跨平台的傳輸(e.g 手機或 IoT),適合 API-based 的架構
* 缺點:無法註銷特定用戶的 token 只能等到 token 自己過期, server-side 失去了對 session 的掌控權。
### 運作方式
* Token-based authentication (stateless authentication),由於 server-side 只負責產生 Token 不儲存任何用戶資訊,所有用戶資訊以及Token 皆儲存在 Client-side (i.e LocalStorage, SessionStorage, Cookies)。


### JWT 格式
#### 1. Header: 定義是否要 Sign跟Encrypted
結構為:
```//
{
"typ": "jwt",
"alg": "HS256"
}
```
#### 2. Payload: 要哪些資料
結構為:
```//
{
"user_id": "b08f86af-35da-48f2-8fab-cef3904660bd",
...
}
```
#### 3. Signature: 驗證資料完整的雜湊值
```//
// 定義格式跟驗證的Hash Fun
header = { typ: 'jwt', alg: 'HS256' }
// 定義要壓縮哪些資料
payload = { user_id: "b08f86af-35da-48f2-8fab-cef3904660bd" }
// 對資料進行編碼
encoded_header = Base64.urlsafe_encode64(header.to_json)
encoded_payload = Base64.urlsafe_encode64(payload.to_json)
// 組合編碼資料用於Hash簽證
data = "#{encoded_header}.#{encoded_payload}"
// 定義加密的雜湊Key
key = 'some_secret'
// 進行Hash
signature = mac = OpenSSL::HMAC.hexdigest("SHA256", key, data)
// 對Signature部分進行編碼
encoded_signature = Base64.urlsafe_encode64(signature)
// 組合三部分為完整Token
token = "#{encoded_header}.#{encoded_payload}.#{encoded_signature}"
```
### Token 範例

#### Hint: JWT、JWS 和 JWE 之間的關係
當提到 JWT 與 Token-based authentication時,時常會看到 JWS 和 JWE ,首先簡單地看一下這三個的全名:
JWT: JSON Web Token (RFC 7519)
JWS: JSON Web Signature (RFC 7515)
JWE: JSON Web Encryption (RFC 7516)
JWT 如上所述,是一個 JSON Object 資料壓縮的格式,竟然只是格式的話,JWT 就並不能代表 Token 本身,因為 Token 是遵循這個格式所產生的結果。
而真正的 Token 其實是 JWS 或 JWE,而這兩個有什麼差別呢?其實從字面上就能看得出來,一個是依造 JWT 格式並使用了雜湊演算法所產生的 Token,另一個則是使用 非對稱式加密演算法 所產生的 Token。

## OAuth
### 介紹與使用場景
為了提高網路理論,網路服務會透過 API 來分享彼此資源,在分享資源前,網路服務也必須取得其他服務的授權才能存取其資源,例如使用者要透過 IG 直接在 FB 發文,IG 必須先要求使用者輸入 FB 帳號進行驗證並授權,若使用 Digest 的授權方式,使用者帳號資訊在驗證後仍會在不同服務平台上傳遞,這會有資安疑慮(詳細的資安疑慮),為了解決這個問題 OAuth 1.0 (i.e Open Authorization )就誕生了。
OAuth 1.0 將認證身分的 User Credential (e.g username, password) 和實際執行權限的 Access token 分開,讓其他服務在認證後直接使用 token 進行資源存取,從而避免 User Credential 曝露在其他服務的風險 ([詳細的授權流程](https://carsonwah.github.io/http-authentication.html))
### 運作方式
* OAuth 2.0: 由於 OAuth 1.0 的取得 Token 的流程稍微複雜,OAuth 2.0 簡化了 OAuth 1.0 的流程並將其 Token 稱為 Bearer Token。


## OpenID 1.0 2.0
是一個去中心化的網路身份認證系統(新數位身分證)等等。
### 基本概念
* 使用者 End User: 想要向某個網站的 ***使用者***
* 標識 Identifier: 使用者可以使用這個標識作為認證
* 身份提供者 Identity Provider: 提供OpenID註冊與認證服務的服務商
* 依賴方 Relying Party: 使用者想使用的網站(使用OpenID的Identifier作為驗證)
## PassportJS
此為JS版本實作網站驗證的函式庫,用於實作多種網站的OAuth驗證授權,
使用 [Passport 的重點部分](https://medium.com/麥克的半路出家筆記/筆記-透過-passport-js-實作驗證機制-11cf478f421e)
1. 載入 Passport 模組,並初始化與使用模組中的兩個 middleware:passport.initialize() 和 passport.session()
2. 建立「驗證策略」及「序列化與反序列化」的方法:serializeUser() 和 deserializeUser()
3. 建立驗證所需路由,並使用passport.authenticate() middleware 驗證使用者
4. 建立 ensureAuthenticated() middleware,在路由上針對往後的請求都先行確認使用者是已經通過驗證的狀態 — Route Protection
## SPA Auth設計
1. 可以長時間登入: Refresh_token
2. 多裝置登入不互相影響: 管理Refresh_token
3. 防治XSS Attack: 被盜用Refresh_token的防範情況
### 前端職責
1. 管理AT、RT
2. 在時效內可以使用該權限可用的路由
3. 在對應的使用者Req上附加相對應的AT到 Header Auth: bearer 上
### 後端職責
1. 檢驗與認證client POST Req Body 的 email、pwd (verify pwd hashed 跟資料庫有無相同)
2. 檢驗Token的權限
1. 每個都要使用Public_Key驗證資料無竄改性
3. 傳送更新的 AT、RT給frontend
4. RT應該要配合 Cache(Redis)儲存 HashMap
### Cache servre (Redis)
1. 建立HashMap 紀錄所有user_id、iss、IP、RT、expiredAt
2. refresh:userId:jti
### client
1. 將RT儲存在Cookies
2. 設定 HttpOnly、Secure:true、sameSite:true、path: /refresh等等,只在要refresh時 請求 /refresh endpoint 並防止RT被 惡意JS程式偷取
#### [設計參考](https://dev.to/cotter/localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end-15id)
```//
JWT options
const passportJWTOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: PUB_KEY || secret phrase,
issuer: 'enter issuer here',
audience: 'enter audience here',
algorithms: ['RS256'],
ignoreExpiration: false,
passReqToCallback: false,
jsonWebTokenOptions: {
complete: false,
clockTolerance: '',
maxAge: '2d', // 2 days
clockTimestamp: '100',
nonce: 'string here for OpenID'
}
}
jwt.sign(payload, secretOrPrivateKey,[option])
jwt.verify(token, secretOrPublicKey,[options])
```
#### [前後端實作](https://medium.com/swlh/everything-you-need-to-know-about-the-passport-jwt-passport-js-strategy-8b69f39014b0)
```//
Why is everyone storing refresh token as a column in users table? That make no sense, because users can be logged in only on one device with refresh token. In my opinion better is to store both tokens as http only cookies with refresh token saved only with "/refresh" path and stored in Redis with key like "refresh:userId:jti". You can then store some device informations in the payload to let the end user list all refresh tokens with their devices and ability to instantly revoke
为什么每个人都把刷新令牌作为用户表的一列来存储?这没有意义,因为用户只能在一个设备上用刷新令牌登录。在我看来,更好的做法是将这两个令牌都存储为HTTP cookies,其中刷新令牌只保存为"/refresh "路径,并以 "refresh:userId:jti "这样的键存储在Redis中。然后,你可以在有效载荷中存储一些设备信息,让终端用户列出所有刷新令牌和他们的设备,并能够立即撤销。
```
### 串第三方 SDK
> 透過第三方快速登入後,會先在user建立相對應的資料,然後provider table記錄user是用哪個第三方登入、第三方的id 是什麼。這邊的create/update就自己寫好判斷
最後後端是吐自己系統的oauth2 access token 跟refresh token出去
> 想問user新增會遇到的衝突
> - mail:duplicated 的問題
> - password:外來的使用者這密碼要怎麼新增拿provider 的ID嗎? 還是password就變成是options?
> 舉個例,手機排版可能不太美觀
users
—————————————
| id | username | password | email |
providers
—————————————
| id | provider | provider_id | user_id | email | avatar |
第三方登入後你會取到第三方給你的access token,你要用這組token取到第三方的user資料 包括id, email
如果是以fb登入,那你就會先check providers這個資料表,例如provider: facebook, provider_id: 123456
沒有資料就建新的user, provider
有資料就對應自己的user表直接登入,respond 自己系統的access token, refresh_token出去給前端
用第三方登入而建立的user,password一律隨機亂數,當然你可以讓他登入後設一組密碼
最後來回答你第一個問題,local跟第三方user是可以寫在一起,但兩個方式的登入api應該是要不一樣的
> - Local用username/email + password登入。
> - 第三方是呼叫第三方sdk,登入後取得第三方access token,再用這組token去取第三方user data(id, email),你要再利用這些資料來做驗證
> * 但這兩個方式最後都是吐回自己service access token
> 不是 user 可不可以依哪種 id 分開,而是 user 可以有一個 local id , x 個 google id , y 個 github id , z 個 fb id 。
此外,現在是多設備情境,同一個 google id ,會有多個設備都登入的情況, token 須反映出實情,所以 token table 的 UK 是 ( specific id, specific token ) 而 user id 為 FK 。
> 關於你這道題的提問, 李泰中 大大已經把最核心的部分點出來了。
我這邊幫忙補充說明一下,一般來說不管是local登入或是第三方登入,目的都在於「建檔」
當資料建檔完成後,後續的所有操作都是自身系統拿user_id 去 response所有內容的
所以關鍵只在於 verify。
而你提到的 password的話是否就會是 optional? 這點對但也不完全對
原因是現在大部分的公司,對於第三方登入的實作,都是直接給一組default的password
也就是說不管如何,你user_credential 的這張table 一定會有 password, salt,access_token ...等欄位
所以以github/gitlab 這些來說,如果你是使用第三方登入的方式,去做註冊進行會員建檔,那麼你就得需要在登入後到 user settings中,手動去變更你的password
否則第三方版控軟體,是沒辦法直接透過帳號密碼進行access git repo 的
因為你是透過第三方登入的方式註冊的,所以根本不知道密碼為何