Nhập môn OAuth và OpenID Connect

Banner sự kiện OpenID Summit Tokyo 2020 — Bên trái là OAuthたん, bên phải làコネクたん

Là developer chắc hẳn ai cũng đã động tới phần đăng nhập đăng ký, cũng đã từng nghe tới khái niệm OAuth. Trong bài này, chúng ta cùng ôn lại OAuth là gì OpenID Connect là gì, tại sao lại sinh ra 2 khái niệm này, tại sao nếu ai đó nói “Login bằng OAuth” lại không đúng nhé. 🙅‍♂️

Tại sao cần OAuth

Trước khi có OAuth, chúng ta có vấn đề về Authorization (ủy quyền) như sau.

Làm thế nào để cho phép dịch vụ truy cập vào dữ liệu của mình?
(mà không cần phải chia sẻ mật khẩu)

Gi sử một ví dụ cụ thể đó là ứng dụng game League of Legends: Wild Rift (từ bây giờ mình gọi là LOL cho ngắn nhé, game này mình cũng hay chơi =))) muốn truy cập vào danh sách bạn bè trên Facebook của người dùng, xem ai cũng chơi LOL để kết bạn trong game. Làm thế nào để người dùng ủy quyền cho ứng dụng LOL rằng tôi đồng ý cho ứng dụng truy cập dữ liệu mà không chia sẻ thông tin tài khoản như mật khẩu?

Làm thế nào để ủy quyền truy cập danh sách bạn bè trên Facebook cho ứng dụng LOL mà không chia sẻ mật khẩu?

Chia sẻ mật khẩu

Nếu ứng dụng LOL hỏi thông tin tài khoản cả username lẫn password như thế này thì các bạn thấy có ổn không?
Cam kết từ ứng dụng LOL: “Chúng tôi cam kết chỉ truy cập danh sách bạn bè của bạn và không chia sẻ dữ liệu cho bên thứ 3. 😇

Câu trả lời đương nhiên là KHÔNG! Nếu ứng dụng LOL là một ứng dụng ác ý, sau khi truy cập vào tài khoản, có toàn quyền như người dùng thật sự, giả danh người dùng (impersonate) có thể tự đăng bài, tự like page, tự thêm bạn … hoặc đổi mật khẩu của người dùng, chiếm quyền kiểm soát của người dùng. Và việc LOL có thật sự không chia sẻ mật khẩu, dữ liệu của người dùng hay không thì đây chỉ là cam kết giữa LOL với người dùng thôi, người dùng chỉ còn cách tin tưởng vào LOL.

Hơn nữa, với người dùng thông thường, rất có thể người dùng sử dụng cùng một email/password để sử dụng các dịch vụ khác, từ các dịch vụ cá nhân (instagram, twitter, zalo, …), hoặc dịch vụ liên quan đến tài chính, ngân hàng. Sử dụng mật khẩu người dùng đã nhập, LOL có thể thử đăng nhập trên các dịch vụ khác và chiếm quyền sử dụng tài khoản trên các dịch vụ đó.

Vì nhiều vấn đề kể trên, chúng ta không nên sử dụng và không nên tạo ra những ứng dụng như vậy.

Ngoài lề một chút thì tuy nói vậy nhưng hiện nay vẫn có những dịch vụ mà vẫn hỏi email và password của người dùng.

Những ứng dụng như MoneyLover tự động tổng hợp giao dịch thu chi của người dùng từ các nguồn như ngân hàng, thẻ … người dùng không cần vào từng tài khoản tự ghi chép lại từng giao dịch. Ứng dụng sẽ tự động thu thập, tổng hợp và hiển thị trực quan cho người dùng. Những ứng dụng như vậy được gọi là Aggregator. Nghiệp vụ này xuất hiện từ khá lâu trong ngành công nghệ tài chính thế giới (Fintech), tiêu biểu ở Việt Nam thì có MoneyLover, bên Mỹ thì có Mint, bên Nhật thì có công ty MoneyForward.

Các dịch vụ như ngân hàng vẫn chưa cung cấp API để aggregator thu thập dữ liệu nên aggregator chỉ còn cách hỏi password plain text của user và trực tiếp đăng nhập tài khoản và crawler dữ liệu về. Từ lâu các ngân hàng không ưa gì các ứng dụng aggregator do rủi ro về bảo mật cũng như trách nhiệm pháp lý khi xâm nhập trái phép xảy ra.
https://tinhte.vn/thread/giao-password-tai-khoan-ngan-hang-cho-app-ben-thu-ba-rui-ro-la-gi.2624434/

Vậy làm thế nào để giải quyết vấn đề này? Các ngân hàng nên cung cấp giải pháp ủy quyền như thế nào?

Làm thế nào để cho phép dịch vụ truy cập vào dữ liệu của mình?
(mà không cần phải chia sẻ mật khẩu)

Đó là lý do OAuth ra đời, OAuth được sinh ra để giải quyết vấn đề ủy quyền.

OAuth 2.0

Có 2 phiên bản OAuth là OAuth 1.0 và OAuth 2.0. Spec (specification — tạm dịch là đặc tả kỹ thuật) của 2 phiên bản là hoàn toàn khác nhau, phiên bản 2.0 không tương thích ngược với phiên bản 1.0 và hai phiên bản không dùng chung với nhau được.

Nếu bạn tìm kiếm OAuth trên mạng thì hầu hết là tài liệu về OAuth 2.0. OAuth 1.0 đã không còn được khuyến cáo sử dụng nữa nên bài này mình sẽ nói về OAuth 2.0 nhé.

Trở lại về bài toán ban đầu, chắc hẳn ai cũng một vài lần bắt gặp ứng dụng xin cấp quyền truy cập dữ liệu của mình như sau:

Ở bước 2 chúng ta có thể nhìn thấy trên thanh URL, người dùng đang đăng nhập trên tên miền của Facebook chứ không phải trên ứng dụng LOL. Người dùng hoàn toàn có thể yên tâm đăng nhập. Thông tin tài khoản và mật khẩu không được chia sẻ cho ứng dụng LOL. Đây chính là mô hình Authorization Code flow, mô hình phổ biến nhất của OAuth 2 hiện nay.

Các thuật ngữ của OAuth 2.0

Để hiểu sâu hơn về OAuth, trước hết chúng ta cùng tìm hiểu về các thuật ngữ trong OAuth 2.

  • Resource Owner: là người sở hữu dữ liệu mà ứng dụng muốn, người sẽ thực hiện hành động ủy quyền truy cập cho ứng dụng. (người dùng)
  • Client: là ứng dụng muốn truy cập dữ liệu của Resource Owner. (ứng dụng LOL)
  • Authorization Server: là hệ thống có khả năng xác thực Resource Owner, có khả năng yêu cầu Resource Owner cấp phép quyền truy cập cho Client. (hệ thống của Facebook)
  • Resource Server: là hệ thống lưu trữ tài nguyên, dữ liệu của Resource Owner. (hệ thống của Facebook)
  • Authorization grant: là thứ biểu thị rằng Resource Owner đã cấp quyền truy cập cho Client.
  • Redirect URI: HTTP endpoint trên Client. Sau khi Resource Owner cấp quyền cho Client sẽ gửi response vào endpoint này.
  • Access token: được cấp phát bởi Authorization Server, có vai trò như chìa khóa cho phép Client gọi đến Resource Server để lấy dữ liệu của Resource Owner.

OAuth 2.0 Authorization Code flow

Chúng ta cùng đi sâu tìm hiểu mô hình Authorization Code flow nhé.

Bước 1: Khởi tạo flow

Từ ứng dụng LOL, người dùng ấn vào nút ủy quyền, sau đó LOL sẽ chuyển hướng người dùng sang Facebook. Nội dung HTTP Request như sau:

GET www.facebook.com/login.php
?response_type=code
&redirect_uri=lol.com/callback
&scope="public_profile email user_friends"
...

Bước 2: Đồng ý trao quyền

Trên Facebook, trước tiên người dùng đăng nhập, sau đó được hỏi có đồng ý trao quyền cho ứng dụng LOL hay không. (consent)

Bước 3: Callback

Giả sử người dùng đồng ý trao quyền, Facebook sẽ tạo Authorization Code, đính kèm trong request và chuyển hướng người dùng về Redirect URI của LOL.

HTTP/1.1 302 Found
Location: lol.com/callback
?code=ffsdSfwe23fsdfasd
...

Bước 4: Đổi Authorization Code lấy Access Token

Sau khi nhận được Authorization Code, LOL gọi đến Authorization Server để đổi lấy Access Token.

(request)
POST www.facebook.com
?grant_type=authorization_code
&code=ffsdSfwe23fsdfasd
&client_id=abc123
&client_secret=secret123
(response)
HTTP/1.1 200 OK
{
"access_token": "qweqwrqweqwe",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "poiupoiupoipoop",
}

Bước 5: Truy cập dữ liệu

Sử dụng Access Token, LOL gọi đến Resource Server truy cập dữ liệu mong muốn (friend list)

POST www.facebook.com/user_friendsAuthorization: Bearer qweqwrqweqwe

⁉️ Ở bước 5, nếu LOL không truy cập danh sách bạn bè của người dùng mà LOL gọi đến endpoint like page hoặc đăng status có được không?

Câu trả lời là không! Access Token được gắn với scope. Scope là những quyền mà Client có thể truy cập. Chúng ta không muốn trao toàn bộ quyền hoặc không có quyền gì cả. Scope là một cách để chia nhỏ các quyền. Ở bước 1, LOL đã yêu cầu 3 scope đó là public_profile email user_friends. Authorization sẽ lưu Access Token cùng với danh sách scope, khi request tới Resource Server, Access Token sẽ được kiểm tra xem có quyền để gọi tới Resource Server không. LOL chỉ được phép gọi trong danh sách scope chỉ định.

Mặt khác, danh sách scope mà Client yêu cầu sẽ được hiển thị ở màn hình ủy quyền, cùng giải thích chi tiết những thông tin mà Client có thể truy cập. Nếu ứng dụng yêu cầu quá nhiều quyền không cần thiết, rất có khả năng người dùng sẽ không đồng ý trao quyền. Do vậy Client chỉ nên yêu cầu đủ các quyền cần thiết.

⁉️ Tại sao cần bước đổi Authorization Code lấy Access Token? Sau khi người dùng ủy quyền, thay vì Authorization Code mà ta trả về Access Token có được không?

Để trả lời được câu hỏi này, chúng ta cần tìm hiểu thêm về một số thuật ngữ khác của OAuth 2.0

Thêm nữa về thuật ngữ OAuth 2.0

  • Back channel (kênh có bảo mật cao): giao tiếp giữa server với server thông qua API, thông tin được mã hóa HTTPS và người dùng không truy cập được mã nguồn.
  • Front channel (kênh có bảo mật thấp): giao tiếp giữa các môi trường như browser/mobile tới server. Mã nguồn ở các môi trường này người dùng có thể truy cập được. (Trên web F12 mở developer console lên là xem đc hết mã nguồn HTML JS…)

Bước 1 và 2 đều được thực hiện ở Front channel, chuyển hướng qua lại giữa các trang web. Tất cả những thông tin như response_type redirect_uri scope ... ở request đều xem được ở URL, tuy nhiên những thông tin này có thể public được, dù có bị lộ cũng không sao cả. Bước 3 cũng được thực hiện ở Front channel, Authorization Code được kèm trong query params và cũng có thể xem được ở URL.

Tại sao không gửi ngay Access Token ở bước 3 thì lý do là vì Front channel là một môi trường không bảo mật, chẳng hạn bằng một thủ thuật nào đó, người dùng bị cài một extension browser độc hại, có thể nghe lén đường truyền hoặc đọc lịch sử browser, hoặc do tác nhân vật lý có ai đó đứng đằng sau lưng người dùng và nhìn trộm người dùng thao tác chẳng hạn. Do vậy ở bước 3 nếu gửi Access Token ở Front channel, ngay trong URL thì rất dễ bị đánh cắp.

Vì vậy chúng ta cần bước 4 được thực hiện ở Back channel, đường truyền được bảo mật và dữ liệu gói tin được mã hóa.

⁉️ Kẻ tấn công đánh cắp Authorization Code và đổi lấy Access Token trước Client có được không?

Câu trả lời cũng là không. Giả sử ở bước 3, Authorization Code bị đánh cắp (bằng các cách kể trên), kẻ tấn công không thể tự mình gửi request đến Authorization Server để đổi lấy Access Token được. Lý do là vì request cần 2 params là client_idclient_secret :

POST www.facebook.com
?grant_type=authorization_code
&code=ffsdSfwe23fsdfasd
&client_id=abc123
&client_secret=secret123

client_id có thể public được, ở bước 1 trong request ở Front channel cũng có client_id trong params, bị lộ cũng không sao cả. Nhưng client_secret không được bị lộ, client_secret là thứ để xác thực Client với Authorization Server, nôm na thì cũng giống mật khẩu vậy. client_secret được giữ an toàn trong Backend của Client, kể cả kẻ tấn công có lấy được Authorization Code đi nữa thì vẫn cần client_secret để đổi lấy Access Token. Một lý do nữa mà client_secret cần đặt ở Back channel đó là vì ở môi trường Front channel không có phương pháp nào để giấu secret cả, đặt trong HTML hay JS thì cũng đều đọc được mã nguồn cả.

Tổng kết lại thì mô hình hóa bằng sequence diagram chúng ta có sơ đồ như sau:

OAuth 2.0 Authorization Code flow sequence diagram

Một vài mô hình OAuth 2.0 khác

Ngoài mô hình phổ biến nhất là Authorization Code chúng ta còn có các mô hình:

  1. Implicit flow
  2. Password
  3. Client Credentials
  4. Device Grant

Mô hình 1 và 2 đã không còn được khuyến cáo sử dụng từ OAuth 2.1

⇒ Kết luận:
Trở lại vấn đề gốc:
Làm thế nào để cho phép dịch vụ truy cập vào dữ liệu của mình? (mà không cần phải chia sẻ mật khẩu)

OAuth được sinh ra để giải quyết vấn đề ủy quyền.

Chú ý là bài toán ủy quyền cho LOL truy cập danh sách bạn bè trên Facebook mình nói bên trên hoàn toàn về vấn đề ủy quyền, mình không nói về đăng nhập hay xác thực cả.

Đăng nhập bằng OAuth 2.0? 🙅‍♂️❌⛔️

OAuth giải quyết rất tốt vấn đề ủy quyền (authorization) và ngày càng được sử dụng một cách rộng rãi, bao gồm cả cung cấp cơ chế xác thực người dùng (authentication). Tuy nhiên điều này không hoàn toàn đúng và khiến nhiều developer lầm tưởng rằng OAuth giải quyết vấn đề xác thực hoặc nói rằng “Login, đăng nhập bằng OAuth”. 🙅‍♂️🙅‍♂️🙅‍♂️

Sự nhập nhằng này phần lớn là do OAuth được sử dụng bên trong cơ chế xác thực, developer thấy các component của OAuth, implement OAuth flow và cho rằng cơ chế xác thực là OAuth. Điều này không đúng, chúng ta cùng xem nếu chỉ sử dụng OAuth thì bài toán xác thực sẽ giải quyết như thế nào.

Ở bước 11, Client muốn lưu User vào database, do vậy cần những thông tin như uid email_address user_name ... nhưng Client nhận được Access Token, một chuỗi ký tự random và không có ý nghĩa gì cả. Cơ chế xác thực cần biểu thị ai là người đã đăng nhập cùng một vài thông tin về người đã đăng nhập, thời điểm đăng nhập. Nếu chỉ xài OAuth thì không hề có các thông tin này.

Do đó, vào năm 2014, Facebook tự implement cơ chế đăng nhập bằng Facebook dựa trên OAuth 2.0 và Google Microsoft LinkedIn … cũng dựa trên OAuth 2.0 tự xây dựng cơ chế đăng nhập “Sign in with … “. Mỗi công ty lại có cách implement hơi khác nhau và không có chuẩn để lấy thông tin người dùng.

Đó là lý do tại sao lại sinh ra OpenID Connect (OIDC), OIDC được sinh ra để giải quyết vấn đề xác thực người dùng.

OAuth 2.0 và OpenID Connect

OAuth 2.0 giải quyết rất tốt vấn đề ủy quyền (Authorization) và ngày càng được sử dụng rộng rãi nhưng lại thiếu cách thức để xác thực, cần giải pháp để khắc phục điều này nhưng vẫn cần đảm bảo tương thích với OAuth 2.0. Do đó, OpenID Connect được sinh ra trên OAuth 2.0, là một extension nhỏ trên OAuth 2.0

OpenID Connect Authorization Code Flow

OpenID Connect thêm một vài khái niệm sau:

  • ID Token
  • Endpoint để lấy thông tin người dùng
  • Chuẩn các scope
  • Chuẩn cách thức implement

Chúng ta cùng xem OpenID Connect hoạt động như thế nào:

OpenID Connect Authorization code flow

ID Token

Ở sequence diagram phía trên, nếu request được thêm scope openid thì sau khi đổi Authorization Code, Client sẽ nhận được Access Token và ID Token. ID Token được mã hóa bằng JWT, bên trong có chứa thông tin của người dùng.

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlz
cyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4
Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAi
bi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEz
MTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6
ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJm
ZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6
ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9l
eGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ.rHQjEmBqn9Jre0OLykYNn
spA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcip
R2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2mac
AAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOY
u0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD
4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl
6cQQWNiDpWOl_lxXjQEvQ

Một tip nhỏ đó là nếu bạn thấy chuỗi kí tự bắt đầu bằng eyJ thì khả năng cao là được mã hóa bằng JWT. Giải mã JWT ta có JSON:

{
"iss": "<https://server.example.com>",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"gender": "female",
"birthdate": "0000-10-31",
"email": "janedoe@example.com",
"picture": "<https://example.com/janedoe/me.jpg>"
}

Decode JWT online bằng trang web https://jwt.io/

Ở bước 12, sau khi lấy được thông tin của người dùng, nếu cần nhiều thông tin về người dùng hơn, Client có thể dùng Access Token để gọi tới /userinfo endpoint.

Mặc dù Client nhận được Access Token và ID Token nhưng không nhất thiết Client phải sử dụng Access Token, nếu nhu cầu chỉ là đăng nhập thì xài ID Token đã là đủ rồi. Tuy nhiên để tương thích với OAuth thì OpenID Connect luôn cấp phát ID Token cùng với OAuth Access Token.

Kết luận

OAuth: Authorization. Who grants what permissions to whom.
OIDC: Authentication. Who one is.

Hy vọng qua bài viết này bạn có được cái nhìn tổng quan về OAuth và OIDC, lý do tại sao nó được sinh ra và nó giải quyết vấn đề gì. Còn rất rất nhiều vấn đề cũng như chủ đề liên quan đến OAuth. Hãy đón đọc các phần tiếp theo nhé. :D

Tham khảo

RFC 6749 The OAuth 2.0 Authorization Framework
OAuth 2.0 oauth.net
User Authentication with OAuth 2.0
OAuth認証とは何か?なぜダメなのか — 2020冬
OAuth 2.0 全フローの図解と動画
OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る
What the hell is OAuth?
OAuth 2.0 and OpenID Connect (in plain English)

Loves building things beautifully. Find me at https://truongnmt.github.io/about/me/

Loves building things beautifully. Find me at https://truongnmt.github.io/about/me/