Client Credentials 核發流程(一)


在今日,經常見到有「第三方應用程式」想從社交網站取得使用者的私人資源,例如,有些資訊是你登入之後才能看到,而社交網站有些遊戲,需要你的這些資訊,你應該不會想玩個遊戲,就到處給這些遊戲帳號密碼吧!

另外,你有些服務只允許登入的使用者操作,而且各設定了不同的權限,使用者可能同時運用多個服務,若使用者在每個服務上,都得進行授權,勢必會造成使用者的困擾,你應該不會想要這麼做!

在這類場合中,會需要將授權流程獨立出來,而不是「客戶端(Client)」與「資源擁有者(Resource Owner)」之間直接進行授權的動作,被獨立出來授權職責,會由「授權伺服器(Authorization Server)」負責,在採取某個驗證流程無誤之後,授權伺服器發給客戶端 Access Token,客戶端拿著 Access Token 對資源擁有者請求,資源擁有者確認 Access Token 合法性之後,才給予受保護的資源(Protected Resources )。

當然,這是個籠統的流程概念,可以有許多做法,在 HTTP 的做法上,OAuth 2 將之標準化,客戶端、資源擁有者、授權伺服器、受保護的資源或者是資源伺服器(Resource Server)等,在 OAuth 2 中都有其角色定義,而取得授權,或更具體地說,核發 Acccess Token 的方式,OAuth 2 規範了四個核發(Grant)類型流程:

  • Authorization Code Grant Type Flow
  • Implicit Grant Type Flow
  • Resource Owner Password Credentials Grant Type Flow
  • Client Credentials Grant Type Flow

英文名稱很長,為了方便,接下來的文件,我會用底下四個來代稱:

  • Authorization Code 核發流程
  • Implicit 核發流程
  • Password Credentials 核發流程
  • Client Credentials 核發流程

實際上,如果你搜尋 OAuth 2,可以找到許許多多關於這四個核發流程的說明,一般都是依以上列點順序來談,實際上,四個核發流程是運用在不同的場合,Authorization Code 核發流程的安全性最高,然而流程最繁複,實作起來的步驟也多,Implicit 核發流程是其簡化版本,然而安全性低了一些(曝露了 Acccess Token),對於密碼在自己控管中的場合,為了統一管理授權可採用 Password Credentials 核發流程,至於不涉及使用者的內部伺服器間授權管理,或者內部完全可信任的客戶端,則採用 Client Credentials 核發流程。

OAuth 2 規範的核發流程,耐心些看網路上的文件,應該可以看懂,如果你接過社交網站的 API,對於作為第三方應用程式,如何與既有的授權伺服器、資源伺服器互動,應該會有具體的概念。

然而,如果你同我一樣,好奇於授權伺服器、資源伺服器等具體上怎麼實作,藉由 Spring Cloud 對 OAuth 2 的支援來認識,是個不錯的出發點,這對於社交網站等 API 的核發流程,在掌握上也有幫助。

只不過,一開始就從 Authorization Code 核發流程開始,由於涉及的流程步驟較多,銜接的環節也多,不如把順序反過來,先從 Client Credentials 核發流程開始實作,逐步探討在什麼樣的需求下,可以進一步採用哪個核發流程。

Client Credentials 核發流程就像是 BASIC 驗證的延伸,在〈閘道與 Spring Security〉中,曾經看過 BASIC 驗證如何應用在閘道上控管已授權的客戶端,Client Credentials 核發流程也可以應用在這類情境,不過是將授權管理拉出來,由授權伺服器實現,授權管理器核發 Access Token,類似 BASIC 驗證,客戶端可以在 Authorization 標頭中設定 Access Token 來請求閘道,若是合法的 Access Token 就允許通過閘道。

不過這邊暫時不談怎麼在閘道上設定,先將焦點放在 Client Credentials 的實作本身。

如果想要實作 Client Credentials 核發流程中的驗證伺服器,使用 Spring Tool Suite 的話,可以建立專案時選擇 Cloud Auth2 以及 Web 的 Starter,然後還要加上點東西在 build.gradle 中:

implementation('javax.xml.bind:jaxb-api:2.2.11') 
implementation('com.sun.xml.bind:jaxb-core:2.2.11')     
implementation('com.sun.xml.bind:jaxb-impl:2.2.11') 
implementation('javax.activation:activation:1.1.1')  

implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.cloud:spring-cloud-starter-oauth2')

如果 Java 版本比 Java 8 高,要加上 JAXB 等的相依,理由在〈服務註冊伺服器〉談過;連接埠就設為 8081 好了,在 application.properties 中加入:

server.port=8081

接著在啟動的主類別上標註 @EnableAuthorizationServer,並加上驗證服務器相關設定:

package cc.openhome;

...

@SpringBootApplication
@EnableAuthorizationServer
public class AuthSvrApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthSvrApplication.class, args);
    }

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public AuthorizationServerConfigurerAdapter authorizationServerConfigurer() {
        return new AuthorizationServerConfigurerAdapter() {

            @Override
            public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                clients.inMemory()
                       .withClient("webclient")
                       .secret(passwordEncoder.encode("webclient12345678"))
                       .scopes("account", "message", "email")
                       .resourceIds("resource")
                       .authorizedGrantTypes("client_credentials");
            }

            @Override
            public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
                oauthServer.checkTokenAccess("isAuthenticated()");    
            }
        };
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

在這邊要注意,使用的是 AuthorizationServerConfigurer,而不是 WebSecurityConfigurer,為了讓事情簡單一些,先將驗證、授權資訊儲存在記憶體中。

在 OAuth 2 中,使用者與客戶端的概念是分離的,使用者指的通常是個擁有註冊帳戶的人,客戶端通常是指使用者操作的應用程式(前端網頁、App、Web 程式等),當然,使用者也可能是個機器人自動程式之類,不過就先當沒這回事,比較不會混淆。

Client Credentials 並不涉及使用者,withClient 中設定的看來像是使用者名稱,不過它實際上指的是已註冊的客戶端名稱,在社交網站上要接 API,不是得先按照它的規定建立個應用程式並給予名稱嗎?大概就是這個東西,至於 secret 看來像是使用者密碼,不過實際上不是,概念上像是 API 密鑰之類的東西。

(我只說概念上像是,因為實際上應用程式名稱、金鑰等,在社交網站各自真正的實作上,可能並不是這樣直接對比的東西。)

scopes 設定類似於 Java EE/Spring Security 中的角色(Role),不過,角色的概念是基於使用者,而 scopes 的概念是基於客戶端,通常是指定可用操作之名稱或類型,之後就可以像 Java EE/Spring Security 那樣,基於 scopes 來指定哪些資源,必須是擁有哪些 scopes 的客戶端可以存取。

resourceIds 可用來設定核發的 Access Token 適用在哪些資源,如果沒有設定的話,就沒有限制。authorizedGrantTypes 設定可以使用哪些核發流程,可以設定多種,這邊先只設定 client_credentials

從 Spring Security 5 開始,強制必須對密碼進行編碼,在整合 OAuth 2 時,密鑰也必須編碼,因此設定了 passwordEncoder

被保護的資源可以在驗證伺服器上,也可以在其他獨立的資源伺服器上,這邊打算示範後者,資源伺服器收到 Access Token 時,必須與驗證伺服器核對,為此必須開放端點,這就是 oauthServer.checkTokenAccess("isAuthenticated()") 設定之目的,開放的端點會是 /oauth/check_token

這麼一來,一個簡單的驗證伺服器就設定完成了,至於客戶端如何請求 Access Token,如何拿著 Access Token 來請求資源伺服器,而資源伺服器又怎麼跟驗證伺服器核對,以及如何根據 scope 來判斷是否有存取資源的權限,就留在下一篇文件再來談。