Wednesday, November 2, 2016

Bài 31: Xác thực một lần và cấp quyền với OAuth 2.0

Trong vài năm trở lại đây, các trang web và ứng dụng di động thường cho phép người dùng sử dụng tài khoản các mạng xã hội hoặc các dịch vụ phổ biến khác để sử dụng dịch vụ thay vì phải đăng ký tài khoản. Hẳn là bạn không còn ngạc nhiên khi thấy dịch vụ mới cho phép bạn dùng tài khoản Google hoặc Facebook để đăng nhập và sử dụng thay vì phải đăng ký thông tin tài khoản. Đứng ở vai trò lập trình viên web, bạn có thắc mắc sao họ làm được như vậy không? Câu trả lời thường là OAuth. 

Xác thực một lần


Với người dùng, việc mỗi lần sử dụng dịch vụ mới lại phải đăng ký thông tin tài khoản khiến họ mệt mỏi khi phải nhớ quá nhiều thông tin tài khoản khác nhau. Người dùng sẽ có xu hướng khai báo cùng một thông tin cho tài khoản ở nhiều dịch vụ. Đây là việc vô cùng nguy hiểm vì nếu bị lộ thông tin ở một dịch vụ, người dùng cũng có thể bị chiếm đoạt tài khoản ở các dịch vụ khác dùng chung thông tin này. Với các nhà cung cấp dịch vụ, họ ngày càng có xu hướng mở rộng dịch vụ qua nhiều lĩnh vực khác nhau tạo thành các dịch vụ con. Các dịch vụ con này hoạt động gần như độc lập với nhau nên việc quản lý người dùng cũng rất phức tạp: quản lý riêng thì không tận dụng được sức mạnh của cụm dịch vụ mà quản lý chung thì cơ chế như thế nào? Xuất phát từ nhu cầu cả hai phía đó, các hệ thống cung cấp xác thực một lần ra đời.

Single Sign On (SSO) hay xác thực một lần là cơ chế xác thực yêu cầu người dùng đăng nhập để xác thực chỉ một lần rồi sau đó có thể truy cập vào nhiều ứng dụng trong một phiên xác thực. Ban đầu SSO chủ yếu dùng trong nội bộ một dịch vụ có nhiều dịch vụ con nhưng dần dần chúng được mở rộng ra cho cả dịch vụ bên ngoài sử dụng. Có nhiều cơ chế hỗ trợ SSO khác nhau nhưng nổi bật nhất là OpenID và SAML. 

Về cơ bản, cơ chế hoạt động của SSO là dịch vụ sử dụng nhận được thẻ bài (token) từ dịch vụ cung cấp SSO sau khi người dùng đăng nhập thành công trên dịch vụ đó. Sau đó dịch vụ sử dụng gửi token này về dịch vụ cung cấp SSO để toàn quyền tiếp cận thông tin người dùng y như người dùng đang thao tác. SSO chỉ phù hợp cho các dịch vụ thuộc cùng một tổ chức bởi dịch vụ sử dụng có toàn quyền tiếp cận thông tin người dùng. Với các dịch vụ bên ngoài thì tốt nhất nên sử dụng cơ chế cấp quyền để giới hạn quyền tiếp cận thông tin người dùng.


OAuth


OAuth là cơ chế cấp quyền người dùng chứ không phải cơ chế xác thực như SSO. Sau khi người dùng đăng nhập thành công với dịch vụ cung cấp OAuth, dịch vụ này sẽ trả về cho dịch vụ sử dụng một token để có thể truy cập thông tin người dùng trên dịch vụ cung cấp. Điểm khác biệt giữa OAuth và các cơ chế SSO là cơ chế cấp quyền như OAuth chỉ cho phép dịch vụ sử dụng tiếp cận một số thông tin giới hạn của người dùng và người dùng quyết định cho hay không. Còn dịch vụ xác thực thì cho phép toàn quyền tiếp cận. Một số nơi vẫn xem OAuth là cơ chế xác thực bởi thông qua cơ chế cấp quyền này, dịch vụ sử dụng cũng xác thực được người dùng.

Hiện nay có hơn 50 nhà cung cấp dịch vụ cấp quyền hỗ trợ OAuth. Tiêu biểu có thể kể đến Google, Facebook, Twitter, Microsoft, Amazon, Dropbox, LinkedIn, Foursquare, Github, Instagram, Netflix, v.v...

Phiên bản hiện tại của OAuth là 2.0, được làm mới từ phiên bản OAuth 1.0. Cơ chế cấp quyền OAuth 2.0 được minh họa qua ví dụ sau:

Qua các bước ở trên chúng ta thấy có 4 đối tượng trong quá trình cấp quyền với OAuth 2.0:
- Người dùng: trong thuật ngữ OAuth gọi là Resource Owner bởi người dùng chính là chủ tài nguyên.
- Server: là nơi cung cấp dịch vụ ứng dụng cho người dùng.
- Client: là trình duyệt, ứng dụng mobile hoặc dịch vụ khác. 
- Dịch vụ cấp quyền: là nơi lưu thông tin tài khoản người dùng, trong thuật ngữ OAuth gọi là Authorization server. Để tiện gọi tên, tôi sẽ gọi nó là OAuth server.

Để có thể yêu cầu OAuth server cung cấp thông tin, client phải đăng ký với nó để được cấp quyền truy cập thông tin người dùng qua cặp ID và khóa. Cặp ID và khóa này là duy nhất trên OAuth server. Trong quá trình đăng ký, client phải cung cấp một số thông tin như tên dịch vụ, địa chỉ trang web, các quyền muốn truy cập, v.v...

Có 4 hình thức cấp quyền khác nhau và mỗi cách sẽ có bước xử lý khác nhau:
Mã ủy quyền (Authorization Code): Các bước xác thực gần như ví dụ minh họa ở trên, chỉ bổ sung thêm vài bước. Cụ thể:
 + Ở (8), thay vì gửi token truy cập thì OAuth server gửi mã ủy quyền. 
 + Client nhận được mã ủy quyền này phải gửi trực tiếp mã ủy quyền kèm ID và khóa bí mật của nó lên OAuth server. 
 + Kiểm tra mã ủy quyền hợp lệ, OAuth server mới gửi token truy cập về cho client.
Ủy quyền ngầm (Implicit)Ủy quyền ngầm chính là hình thức cấp quyền trong ví dụ bên trên. Thay vì gửi mã ủy quyền, OAuth server gửi thẳng token truy cập cho client luôn. Ủy quyền ngầm giúp rút ngắn quá trình xác thực và bảo mật hơn do client gửi ID và khóa của nó lên OAuth server có thể tiềm ẩn rủi ro. Hiện tại đây là cách phổ biến.
Ở 2 hình thức cấp quyền này, người dùng đăng nhập OAuth server nhờ trang web của OAuth server hoặc cửa sổ đăng nhập riêng của OAuth server gắn trên client. Sở dĩ có được chuyện này là sau khi đăng ký sử dụng thì client được yêu cầu sử dụng SDK của dịch vụ cung cấp OAuth để thực hiện công đoạn này. Mục đích của phần này là đảm bảo client không biết được thông tin đăng nhập của người dùng. Sau khi đăng nhập thành công, OAuth server sẽ yêu cầu người dùng cho phép client quyền truy cập thông tin của người dùng trên dịch vụ. Kết quả người dùng cho phép hay từ chối sẽ chuyển về client theo URL đã đăng ký trước đó. Mã ủy quyền hay token truy cập sẽ được gửi kèm nếu người dùng cho phép truy cập thông tin của họ.
Thông tin đăng nhập của người dùng (Resource Owner Password Credentials): Đây là cách cấp quyền truy cập mà ở đó người dùng cung cấp thông tin đăng nhập cho client và client dùng nó để đăng nhập dịch vụ xác thực. Cách này chỉ được sử dụng khi người dùng tin tưởng cao ở client.
Thông tin đăng nhập của client: Đây là cách client sử dụng để cập nhật thông tin của nó trên dịch vụ xác thực, ví dụ như URL chuyển hướng.

OAuth chỉ đề cập đến quy trình đăng nhập trên OAuth server và cấp quyền cho dịch vụ sử dụng thông tin người dùng trên OAuth server. Một khi OAuth server xác thực client và cung cấp quyền cho dịch vụ, dịch vụ xem như người dùng đã được xác thực. Ở các lần truy cập tới, xác thực như thế nào là do dịch vụ tự quyết định và thông thường, họ sử dụng token để duy trì xác thực người dùng và JWT là cách thường được lựa chọn.


Cấp quyền OAuth 2.0 với Go


Có rất nhiều package hỗ trợ việc sử dụng OAuth 2.0 trong Go. Ở đây tôi sử dụng goth. Goth hỗ trợ kết nối hơn 20 dịch vụ OAuth server phổ biến. Bạn có thể xem danh sách này ở goth/providers. Bây giờ chúng ta sẽ cùng thử kết nối sử dụng OAuth 2.0 với Google Plus, Facebook, Twitter qua goth:
- Đầu tiên như thường lệ, chúng ta cần lấy package về qua go get github.com/markbates/goth go get github.com/gorilla/pat. Pat là package con thuộc Gorilla đóng vai trò Server Mux.
- Tiếp theo là khai báo package sử dụng:
import (
    "encoding/json"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "os"

    "github.com/gorilla/pat"
    "github.com/markbates/goth"
    "github.com/markbates/goth/gothic"
    "github.com/markbates/goth/providers/facebook"
    "github.com/markbates/goth/providers/gplus"
    "github.com/markbates/goth/providers/twitter"
)
- Tiếp theo là phần nạp thông tin khóa và khóa bí mật các dịch vụ OAuth server cấp cho ứng dụng. Các thông tin này được lưu trong file JSON config.json có cú pháp như sau:
{
    "GPlusKey" : "<Khóa được google cấp>",
    "GPlusSecret" : "<Mật khẩu>",
    "FacebookKey" : "<Khóa được facebook cấp>",
    "FacebookSecret" : "<Mật khẩu>",
    "TwitterKey" : "<Khóa được twitter cấp>",
    "TwitterSecret" : "<Mật khẩu>",
}
Thông tin được nạp vào biến toàn cục kiểu struct Configuration qua hàm init như sau (hàm init được gọi trước hàm main):
type Configuration struct {
    GPlusKey string
    GPlusSecret string
    FacebookKey string
    FacebookSecret string
    TwitterKey string
    TwitterSecret string
}

func init() {
    file, _ := os.Open("config.json")
    decoder := json.NewDecoder(file)
    config = Configuration{}
    err := decoder.Decode(&config)
    if err != nil {
        log.Fatal(err)
    }
}
- Hàm main có 2 phần: phần đầu khai báo thông tin truy cập và URL phản hồi của 3 dịch vụ với goth. Phần sau là khai báo web server:
    goth.UseProviders(gplus.New(
        config.GPlusKey, config.GPlusSecret, "http://localhost:8080/auth/gplus/callback"),
        facebook.New(config.FacebookKey, config.FacebookSecret, "http://localhost:8080/auth/facebook/callback"),
        twitter.New(config.TwitterKey, config.TwitterSecret, "http://localhost:8080/auth/twitter/callback"),
    )

    r := pat.New()
    r.Get("/auth/{provider}/callback", callbackAuthHandler)
    r.Get("/auth/{provider}", gothic.BeginAuthHandler)
    r.Get("/", indexHandler)
    server := &http.Server{
        Addr: ":8080",
        Handler: r,
    }
    log.Println("Listening...")
    server.ListenAndServe()
Ta thấy pat hỗ trợ URL có tham số để trong cặp {}. Hai hàm handler xử lý trang chủ và phần hiển thị thông tin. Handler còn lại do gothic xử lý:
func callbackAuthHandler(res http.ResponseWriter, req *http.Request) {
    user, err := gothic.CompleteUserAuth(res, req)
    if err != nil {
        fmt.Fprintln(res, err)
        return
    }
    t, _ := template.New("userinfo").Parse(userTemplate)
    t.Execute(res, user)
}
func indexHandler(res http.ResponseWriter, req *http.Request) {
    t, _ := template.New("index").Parse(indexTemplate)
    t.Execute(res, nil)
}
Mọi việc có vẻ rất đơn giản bởi việc kết nối qua các bước của OAuth đã có goth làm. Thông tin người dùng lấy về cũng do goth thực hiện lưu trong cấu trúc User của goth gồm các thông tin cơ bản, các thông tin khác có thể kiếm trong trường RawData:
type User struct {
    RawData map[string]interface{}
    Provider string
    Email string
    Name string
    FirstName string
    LastName string
    NickName string
    Description string
    UserID string
    AvatarURL string
    Location string
    AccessToken string
    AccessTokenSecret string
    RefreshToken string
    ExpiresAt time.Time
}
- Hai mẫu HTML cho trang chủ và trang thông tin như sau:
var indexTemplate = `
<p><a href="/auth/gplus">Log in with Google Plus</a></p>
<p><a href="/auth/facebook">Log in with Facebook</a></p>
<p><a href="/auth/twitter">Log in with Twitter</a></p>`

var userTemplate = `
<p>Name: {{.Name}}</p>
<p>Email: {{.Email}}</p>
<p>NickName: {{.NickName}}</p>
<p>Location: {{.Location}}</p>
<p>AvatarURL: {{.AvatarURL}} <img src="{{.AvatarURL}}"></p>
<p>Description: {{.Description}}</p>
<p>UserID: {{.UserID}}</p>
<p>AccessToken: {{.AccessToken}}</p>`
 - Khi thực thi ứng dụng web server, từ trình duyệt gọi localhost:8080, ta sẽ có trang web như sau
- Chọn vào Log in with Google Plus. Nếu chưa đăng nhập sẽ được yêu cầu đăng nhập tài khoản google. Sau khi đăng nhập xong sẽ xuất hiện thông tin đại khái như sau (tôi sử dụng Google OAuth 2.0 Playground để minh họa):

Một khi cho phép, Google sẽ trả mã ủy quyền về theo địa chỉ URL đã khai cho goth. Ở đây chúng ta cần lấy thông tin người dùng nên goth lấy về và lưu vào biến user qua đoạn mã:
user, err := gothic.CompleteUserAuth(res, req)
- Thông tin người dùng được trả về theo mẫu HTML như sau:


Ở trên chúng ta đã tìm hiểu về SSO và OAuth cũng như cách phát triển chức năng cấp quyền với OAuth. Nếu muốn tìm hiểu về xác thực với các SSO bạn có thể tham khảo OpenID connect. Đây là dịch vụ xác thực được xây dựng trên nền OAuth 2.0.

Trong bài tới chúng ta sẽ tìm hiểu về bảo mật ứng dụng web.

Tóm tắt:
- SSO hay xác thực một lần là cơ chế yêu cầu đăng nhập một lần để có thể sử dụng nhiều dịch vụ khác nhau. Hai cơ chế hỗ trợ SSO nổi tiếng là OpenID và SAML.
- OAuth là cơ chế cấp quyền tức là nó không có quyền tiếp cận mọi thông tin của người dùng như cơ chế xác thực mà chỉ tiếp cận được số lượng thông tin giới hạn được khai báo trước đó và phải xin phép người dùng để tiếp cận các thông tin này.
- Phiên bản hiện tại của OAuth là 2.0, một bước đột phá so với 1.0.
- OAuth có 4 hình thức cấp quyền: mã ủy quyền, ủy quyền ngầm, thông tin đăng nhập của người dùng và thông tin đăng nhập của client. Ủy quyền ngầm là cách phổ biến nhất.
- OAuth trên Go có thể dùng package goth để phát triển. Nó hỗ trợ hơn 20 dịch vụ phổ biến khác nhau.

No comments:

Post a Comment