<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版後,才終於看起來有比較像平常看到的那種專案結構了...