<style>
.red {
color: red;
}
</style>
# SSO 技術實踐: CAS整合JWT(Server篇)
###### tags: `CAS`, `JWT`
## 前言:
在研究CAS的時候撞得滿頭包,老樣子會記錄一些遇到的問題,也會稍微介紹一下CAS protocol整體流程以及和jwt流程的差異性。
對於Single Sign On為何,相信網路上已有諸多解釋,在此不多贅述。
但需要先有一個觀念是,User登入時會有兩種session,一種是SSO session,另一種則是在Client local site session。
因此即便從sso中登出,廢止了sso session,<span class="red">如果Server沒有通知底下全部的Client Service將這個User登出的話,就會發生已登入的Client Service,因為session沒有被廢除,所以依然還在登入狀態,但其他的Client Service卻會重新要求登入,導致狀態不一致</span>。
--------------------------------------------------
## 使用版本及使用套件:
* CAS Server v6.0: https://github.com/apereo/cas-overlay-template/tree/6.0
* CAS Client: https://github.com/cas-projects/cas-sample-java-webapp
* Tomcat Server: CAS Server * 1、CAS Client * 2
CAS Protocol v3.0
* Gradle 6.5
Server使用套件(請加入在build.gradle):
* [JWT as Service Ticket](https://apereo.github.io/cas/6.0.x/installation/Configure-ServiceTicket-JWT.html)[1]
* [LDAP Authentication](https://apereo.github.io/cas/6.0.x/installation/LDAP-Authentication.html#ldap-authentication) [2]
Client使用套件(請加入在pom.xml):
* CAS Client相關設定傳送門: [SSO 技術實踐: CAS整合JWT(Client篇)](/klP2PIEDSsOBgXLyABAQpw)
* [JOSE4j](https://mvnrepository.com/artifact/org.bitbucket.b_c/jose4j/0.7.0) :解析和驗證jwt字串
* [JSON](https://mvnrepository.com/artifact/org.json/json/20190722)
* [OPENSAML J](https://mvnrepository.com/artifact/org.opensaml/opensaml/2.6.4) :接收Server發送的SLO request時需要解析saml字串
--------------------------------------------------
## CAS相關名詞:
Ticket Granting Tikcet(TGT): 用來證明使用者已經在CAS系統登入過,登入成功後系統會將TGT存在快取,並將id的值存入TGC,這樣之後只要比對id就能知道使用者是否登入。
Ticket Granting Cookie(TGC): 就是用來存TGT id的作用,登入後會將此物件返回給使用者,驗證時會攜帶TGC到Server確認是否登入。
Service Ticket(ST): 除了返回TGC以外,CAS還會為當前使用的服務(也就是當下的CAS Client)註冊,產生票據後連同TGC一起返回給使用者。
--------------------------------------------------
## CAS流程介紹:
先附上CAS社群的使用指南: https://apereo.github.io/cas/6.0.x/protocol/CAS-Protocol.html#cas-protocol
從圖片中可以看到整個protocol的時序圖和整體流程,不過我自己畫了張流程圖,接著會以下圖來依序說明:
![](https://i.imgur.com/MSSW5Lz.png)
1. 使用者(browser)先訪問CAS Client(以下簡稱client),會先檢查此session是否在client local存在過。
2. 若尚未登入,此時會將使用者重新導向到CAS Server(以下簡稱Server)。
3. Server會檢查是否攜帶TGC,如果沒有就重新登入,有的話就簽發ST後返回給Client
4. 此時應該已經拿到ST了,驗證過ST之後,Client最後就會發放請求資源給使用者
--------------------------------------------------
## JWT流程介紹:
一樣先附上社群文章:
https://apereo.github.io/cas/6.6.x/installation/Configure-ServiceTicket-JWT.html
jwt的流程也是基於CAS protocol的流程下去做調整,為何會選擇將jwt替換掉ST呢? 其實這兩者的功用是完全一樣的差,差別只在於如果希望拿到ST後<span class="red">不用再回去跟Server驗證一次,而是在Client端的部分驗證jwt字串</span>,字串裡面會包含有ST的資訊以及過期時間等等資訊,只要驗證過後就馬上發放請求資源,<span class="red">進而減少Client和Server溝通的次數</span>。
![](https://i.imgur.com/7ozvCgl.png)
因為流程很類似,只有最後不同,就不多加描述了~
--------------------------------------------------
## CAS Server一些相關設定細節:
接下來這段都是實作中遇到的一些bug shooting和設定問題還有雜談,或許遇到問題時這邊能找到一些答案~
#### 雜談:
* 從社群上可以看到使用了Overlay方式來deploy專案,因為他不希望你動到cas project的原始碼的關係,所以即便你抓了下來,也看不到任何的原始碼,會覺得怎麼專案就這麼空?? 他的概念是如果你有想要針對cas server做設定,或者想要覆蓋原先檔案的寫法的話,必須要在<span class="red">`project path\cas\src\main\resources`</span>這個路徑底下放上去你的設定檔案,例如針對cas server設定很重要的<span class="red">`application.properties`</span>。[4]
* 有一點必須說一下,假設今天當你從git抓下來後,build完之後,他會產出一個cas.jar,只需要把這個jar deploy到tomcat,啟動後就ok了~ 但是這樣對開發來說時常需要去調整設定檔什麼之類的,每改一次就要build一次再重新deploy真的太煩人!!!
所以建議直接將專案導入到eclipse或個人習慣用的ide,同時把cas.jar解壓後,將`cas\WEB-INF\classes`這個資料夾設定成專案的source之一,這樣啟動後才會有cas project預設的一些html可以呈現。
![](https://i.imgur.com/9kFHSbs.png)
#### application.properties相關設定:
1. log設定: 有不少資訊是需要trace層級才能看見,也可另外定義log設定檔
```xml
##
# CAS Log4j Configuration
#
# logging.config=file:/etc/cas/log4j2.xml
server.servlet.context-parameters.isLog4jAutoInitializationDisabled=true
logging.level.org.apereo.cas=DEBUG
```
2. CAS Server域名設定,這段不見得需要加,但有發生過不加的話,好像會吃預設的域名(test.sso.com什麼之類的),而不是自己設定的部分。
```xml
# CAS Server
cas.server.name=https://sso.server.com:8443
cas.server.prefix=https://sso.server.com:8443/cas
```
3. SSL設定: key-store-password是當初設定給keystore這個檔案的密碼,key-password是keystore裡面有記錄著一把私鑰,也可以為他設定密碼,如果當初產keystore沒有特地在把key加密的話,就不需要設定。
```xml
##
# CAS Web Application Embedded Server SSL Configuration
#
server.ssl.key-store=file:keystore的路徑\ssodemo.keystore
server.ssl.key-store-password=123456
#server.ssl.key-password=123456
```
4. CAS認證: 官方會建議你不要使用本地認證的部分,而是搭配像是ldap或其他認證方式都有支援,但如果只是自己要開發測試,不想這麼麻煩,就依照第一個方式設定,這邊也額外補充使用了ad ldap的設定相關方式。
* 這邊<span class="red">強烈建議</span>去看社群[v5.0](https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ldap-connection-pool)的設定屬性表比較齊全[6],當初看6.0的時候少了一些屬性,加上又將相關屬性散落在各個地方,真的不是很好查,在設定的時候真的曾經卡到懷疑人生..
```xml
##
# CAS Authentication Credentials
#
cas.authn.accept.users=test::123456
#cas.authn.accept.name=Static Credentials
# LDAP Authentication Connection Setting
# LDAP 的相關設定請參考cas 5.0版本的會比較齊全
#cas.authn.ldap[0].type=AD
# basedn = ldap物件的基底位址,代表輸入帳號密碼認證時會從這個路徑開始找
#cas.authn.ldap[0].baseDn=cn=xxx,dc=xxx
#cas.authn.ldap[0].subtreeSearch=true
#cas.authn.ldap[0].searchFilter=cn={user}
#cas.authn.ldap[0].enhanceWithEntryResolver=true
#cas.authn.ldap[0].dnFormat=cn=%s,cn=xxx,dc=xxx
#cas.authn.ldap[0].ldapUrl=ldap://ldap的ip
#cas.authn.ldap[0].useSsl=false
#cas.authn.ldap[0].useStartTls=false
#cas.authn.ldap[0].connectTimeout=5000
# binddn = 在AD裡任何一位使用者的DN位址(為了一開始執行使用者身分認證時要進入ldap取得資料,較新的版本會稱為principalName)
#cas.authn.ldap[0].bindDn=cn=xxx,dc=xxx
#cas.authn.ldap[0].bindCredential=上面那組帳號的密碼
#cas.authn.ldap[0].providerClass=org.ldaptive.provider.unboundid.UnboundIDProvider
#cas.authn.ldap[0].connectTimeout=PT10S
# LDAP Authentication principal設定,想回傳想要的屬性可在此設定
#cas.authn.ldap[0].principalAttributeId=sAMAccountName
#cas.authn.ldap[0].principalAttributeList=givenName,cn,mail
#cas.authn.ldap[0].collectDnAttribute=true
#cas.authn.ldap[0].allowMultiplePrincipalAttributeValues=true
#cas.authn.ldap[0].allowMissingPrincipalAttributeValue=true
```
5. Service Registry: <span class="red">這部分非常重要</span>,算是一定要設定部分,因為要讓Server知道到底有哪些client訪問是可以被註冊的,這邊採取用json設定方式(社群網站有其他方式,請自行參閱)。[7]
```xml
# Service Registry
cas.serviceRegistry.watcherEnabled=true
cas.serviceRegistry.schedule.repeatInterval=120000
cas.serviceRegistry.schedule.startDelay=15000
# Auto-initialize the registry from default JSON service definitions
cas.serviceRegistry.initFromJson=true
```
* 這邊還要搭配json檔案的配置,請在`project path\cas\src\main\resources`底下,建立一個資料夾`services`,並且建立json檔案,檔名有規定格式,需命名為:name-id.json*。
像下方name: client1_server、id 1,所以檔名就是: client1_server-1.json。
```json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps)://自己設定的client域名",
"name" : "client1_server",
"id" : 1,
"evaluationOrder" : 1
}
```
6. JWT Ticket設定: 要使用jwt as service ticket[5],務必要在build.gradle裡面加入套件,並且除了在application.properties設定後,service部分也需要設定。
* 如果感覺一直吃不到設定檔的感覺,可以去把classes的資料夾,把他預設的application.properties檔案給砍了,只留下自己寫的即可。
* 如果不知道jwt的signing key和encrpytion key要設定成什麼,<span class="red">可以先在不設定的情況下啟動一次Server,正常順利的話看看console,系統會自動產出一組給你,請把它複製起來貼在json裡面。</span>
```xml
# JWT Tickets
cas.authn.token.crypto.encryptionEnabled=true
cas.authn.token.crypto.signingEnabled=true
```
* 請把這段json code加在service.json裡面
```json
"properties" : {
"@class" : "java.util.HashMap",
"jwtAsServiceTicket" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "true" ] ]
},
"jwtAsServiceTicketSigningKey" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "7gInlM_CH3WTNIiatPWHr_...." ] ]
},
"jwtAsServiceTicketEncryptionKey" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceProperty",
"values" : [ "java.util.HashSet", [ "f5E4tGS9v6IXEcm57ix...." ] ]
}
}
```
7. Single Logout(SLO)設定: <span class="red">這邊一樣非常重要!</span> 如果這邊沒設定的話,即便社群有說SLO預設是啟動的,但沒設定要發送到client的哪個位子,基本上client也收不到,就做不了事情...[8][9]
* 這個設定預設是false,主要是設定如果網址後面帶service=xxxx,當server做完事情後,會導回client中你指定的url。(請參閱[CAS Protocol 2.3.1](https://apereo.github.io/cas/6.0.x/protocol/CAS-Protocol-Specification.html#231-parameters))
```xml
# cas logout setting
cas.logout.followServiceRedirects=true
cas.slo.asynchronous=true
```
* 一樣將下列這段json code加進service.json中,預設是back_channel,這種方式會直接讓server背地裡直接傳request給client,如果是front_channel,要登出前就會跳出視窗讓使用者確認。
```json
"logoutType": "BACK_CHANNEL",
"logoutUrl": "看你想將它導到client的哪個頁面的網址"
```
## Reference:
[1] [JWT Service Ticket](https://apereo.github.io/cas/6.6.x/installation/Configure-ServiceTicket-JWT.html)
[2] [LDAP Authentication](https://apereo.github.io/cas/6.6.x/installation/LDAP-Authentication.html#ldap-authentication)
[3] [CAS Protocol](https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol.html#cas-protocol)
[4] [CAS Overlay](https://apereo.github.io/cas/6.6.x/installation/WAR-Overlay-Installation.html#cas-war-overlays)
[5] [CAS Properties: JWT Ticket](https://apereo.github.io/cas/6.6.x/configuration/Configuration-Properties.html#jwt-tickets)
[6] [CAS Properties: LDAP Connection Pool v5.0](https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ldap-connection-pool)
[7] [CAS Properties: Service Registry](https://apereo.github.io/cas/6.6.x/configuration/Configuration-Properties.html#service-registry)
[8] [CAS Properties: Single Logout](https://apereo.github.io/cas/6.6.x/configuration/Configuration-Properties.html#single-logout)
[9] [Single Logout(SLO)](https://apereo.github.io/cas/6.6.x/installation/Logout-Single-Signout.html)
> [name=夜雨]
> 由於一開始第一次接觸什麼叫Overlay的關係,一開始抓git抓CAS Server專案包下來的時候選了master版本,結果只看到抓下來一大堆各式各樣的xxx server,看得頭昏眼花又搞不太懂,很不像傳統一個網頁project的樣子...重點是build又要超久!! 後來選了6.0版後,才終於看起來有比較像平常看到的那種專案結構了...