Try   HackMD

oAuth 2.0以line Notify為例

oAuth 2.0概述

維基百科對oAuth的說明如下:

開放授權(OAuth)是一個開放標準,允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如相片,影片,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。

OAuth允許使用者提供一個權杖,而不是使用者名稱和密碼來訪問他們存放在特定服務提供者的資料。每一個權杖授權一個特定的網站(例如,影片編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相簿中的影片)。這樣,OAuth讓使用者可以授權第三方網站訪問他們儲存在另外服務提供者的某些特定資訊,而非所有內容。

簡單說,就是在不提供使用者名稱與密碼的條件下,如何授權給第三方應用程式使用資料。

oAuth的官方文件RFC 6750示意圖如下:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

專有名詞:

  • Resource Owner
  • Authorization Server
  • Resource Server
  • Client
  • Authorization Request
  • Authorization Grant
  • Access Token

理論其實不好理解,我們採用一個實際案例 - Line Notify來說明實作oAuth 2.0的過程。

Line Notify概述

透過LINE接收其他網站服務通知與網站服務連動完成後,LINE所提供的官方帳號「LINE Notify」將會傳送通知。不僅可與多個服務連動,也可透過LINE群組接收通知。

任何公司行號或個人都可以透過服務登錄,讓line用戶收到相關通知。對於開發人員,一個有用的應用情境就是背景作業通知。一般而言,許多背景作業是否如期完成,都需要維護人員主動登入監控畫面才能得知,理想的狀況應該是背景作業完成後,觸發一個通知機制,主動告知維運人員完成狀況。

在這個案例中,每一個line帳號,可視為Protected Resource,Line伺服器會同時扮演Resource Server和Authorization Server角色,我們必須開發一個程式(服務)跟line伺服器互動,取得授權之Access Token後發送相關訊息。

Line Notify服務開發流程

  • 登錄服務

對於Line伺服器而言,必須先確認登錄服務是合法。所以會提供以下連結,讓第三方服務登錄。完成登錄後,Line會提供特定的Client id和Client Secret。登錄服務,其中Client id視為服務的唯一識別碼,Client Secret則是取得Access Token之密碼。

  • 取得Access Token

對於Line Notify而言,取得Access Token分成兩個步驟:

  1. User Request Integration(相當於RFC 6750中B與C的動作)
  2. Request Access Token(相當於RFC 6750中D與E的動作)

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

由於oAuth的過程中不會提供資源擁有者的訊息給第三方應用程式(Client),因此實務上的做法會是由第三方應用程式提供網頁(或對話機器人)作為入口。以上圖為例,第三方應用程式(YOUR SITE)提供一超連結,資源擁有者點入後即進入oAuth流程。超連結內容如下:

GET https://notify-bot.line.me/oauth/authorize

Request Parameters如下:

Parameter name Required/Option Type Description
response_type Required fixed value Assigns "code"
client_id Required string Assigns the client ID of the generated OAuth
redirect_uri Required uri Assigns the generated redirect URI
scope Required fixed value Assigns "notify"
state Required string Assigns a token that can be used for responding to CSRF attacks
response_mode Optional string By assigning "form_post", sends POST request to redirect_uri by form post instead of redirecting

User Request Integration動作意味著資源擁有者透過第三方應用程式,以HTTPS GET方式向Authorization Server提出需求。

我們可以詳細看一下每一parameter所代表的意義:
* client_is:服務登錄時line所提供,用來讓line識別服務的合法性。
* redirect_uri:當GET Request完成後,Authorization Server將結果回傳的目的地。
* scope:line notify使用了一個固定值(notify),在RFC 6750是這樣說明的:
The value of the scope parameter is expressed as a list of space-delimited, case-sensitive strings. The strings are defined by the authorization server.
也就是說,對於不同的Authorization Server,會有的內容。
* response_type:line notify使用了一個固定值(code),RFC 6750也是同樣的陳述(Value MUST be set to "code")
* state:line notify並沒有特別的定義,可填任意值。但實務上state存在於reponse的回傳值,對於程式設計而言,可用來確保回傳是合法;同時,第三方應用程式也可以使用該欄位記錄資源擁有者的識別資料。

回傳至redirect_uri的內容會是

code=<Code from authorization server>&state=<state information from client>

其中code就是接下來client用來取得access token所需的資料。RFC 6750對於code有以下說明:
The authorization code generated by the authorization server. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
簡單說就是code是一次性,不可被重複使用。

第二個動作就是使用前面所取得的code,結合client id和client secret,讀取access token,命令如下:

POST https://notify-bot.line.me/oauth/token

Request Parameters如下:

Parameter name Required/optional Type Description
grant_type Required fixed value Assigns "authorization_code" RFC 6750 的說明:Value MUST be set to "authorization_code".
code Required string Assigns a code parameter value generated during redirection
redirect_uri Required uri Assigns redirect_uri to assigned authorization endpoint API
client_id Required string Assigns client ID to issued OAuth
client_secret Required string Assigns secret to issued OAuth

參數多出現在前面的說明內容,就不再贅述。

程式開發

  1. 產生 HTTP GET string
def get_notify_url(u_path):
    params = {
        'response_type': 'code',
        'client_id': <your client id>,
        'redirect_uri': '<Your redirect uri>',
        'scope': 'notify',
        'response_mode': 'form_post',
        'state': <Your state information>
    }
    query_str = urllib.parse.urlencode(params)
    return f'https://notify-bot.line.me/oauth/authorize?{query_str}'
  1. callback for HTTP POST
body = request.get_data(as_text=True)
print(f"Request Body: {body}")

# Parse the URL-encoded query string
query_params = urllib.parse.parse_qs(body)

# Extract the code and state parameters
code = query_params.get('code', [None])[0]
state = query_params.get('state', [None])[0]
  1. Get Access Token
def get_token(code, client_id, client_secret, redirect_uri):
    url = 'https://notify-bot.line.me/oauth/token'
    headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
    data = {
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': redirect_uri,
        'client_id': client_id,
        'client_secret': client_secret
    }
    data = urllib.parse.urlencode(data).encode()
    req = urllib.request.Request(url, data=data, headers=headers)
    page = urllib.request.urlopen(req).read()
    
    res = json.loads(page.decode('utf-8'))
    return res['access_token']