Try   HackMD

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,如果Server沒有通知底下全部的Client Service將這個User登出的話,就會發生已登入的Client Service,因為session沒有被廢除,所以依然還在登入狀態,但其他的Client Service卻會重新要求登入,導致狀態不一致


使用版本及使用套件:

Server使用套件(請加入在build.gradle):

Client使用套件(請加入在pom.xml):


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的時序圖和整體流程,不過我自己畫了張流程圖,接著會以下圖來依序說明:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  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後不用再回去跟Server驗證一次,而是在Client端的部分驗證jwt字串,字串裡面會包含有ST的資訊以及過期時間等等資訊,只要驗證過後就馬上發放請求資源,進而減少Client和Server溝通的次數

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

因為流程很類似,只有最後不同,就不多加描述了~


CAS Server一些相關設定細節:

接下來這段都是實作中遇到的一些bug shooting和設定問題還有雜談,或許遇到問題時這邊能找到一些答案~

雜談:

  • 從社群上可以看到使用了Overlay方式來deploy專案,因為他不希望你動到cas project的原始碼的關係,所以即便你抓了下來,也看不到任何的原始碼,會覺得怎麼專案就這麼空?? 他的概念是如果你有想要針對cas server做設定,或者想要覆蓋原先檔案的寫法的話,必須要在project path\cas\src\main\resources這個路徑底下放上去你的設定檔案,例如針對cas server設定很重要的application.properties。[4]

  • 有一點必須說一下,假設今天當你從git抓下來後,build完之後,他會產出一個cas.jar,只需要把這個jar deploy到tomcat,啟動後就ok了~ 但是這樣對開發來說時常需要去調整設定檔什麼之類的,每改一次就要build一次再重新deploy真的太煩人!!!

    所以建議直接將專案導入到eclipse或個人習慣用的ide,同時把cas.jar解壓後,將cas\WEB-INF\classes這個資料夾設定成專案的source之一,這樣啟動後才會有cas project預設的一些html可以呈現。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

application.properties相關設定:

  1. log設定: 有不少資訊是需要trace層級才能看見,也可另外定義log設定檔
##
# CAS Log4j Configuration
#
# logging.config=file:/etc/cas/log4j2.xml
server.servlet.context-parameters.isLog4jAutoInitializationDisabled=true
logging.level.org.apereo.cas=DEBUG
  1. CAS Server域名設定,這段不見得需要加,但有發生過不加的話,好像會吃預設的域名(test.sso.com什麼之類的),而不是自己設定的部分。
# CAS Server
cas.server.name=https://sso.server.com:8443
cas.server.prefix=https://sso.server.com:8443/cas
  1. SSL設定: key-store-password是當初設定給keystore這個檔案的密碼,key-password是keystore裡面有記錄著一把私鑰,也可以為他設定密碼,如果當初產keystore沒有特地在把key加密的話,就不需要設定。
##
# 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
  1. CAS認證: 官方會建議你不要使用本地認證的部分,而是搭配像是ldap或其他認證方式都有支援,但如果只是自己要開發測試,不想這麼麻煩,就依照第一個方式設定,這邊也額外補充使用了ad ldap的設定相關方式。
  • 這邊強烈建議去看社群v5.0的設定屬性表比較齊全[6],當初看6.0的時候少了一些屬性,加上又將相關屬性散落在各個地方,真的不是很好查,在設定的時候真的曾經卡到懷疑人生..
##
# 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
  1. Service Registry: 這部分非常重要,算是一定要設定部分,因為要讓Server知道到底有哪些client訪問是可以被註冊的,這邊採取用json設定方式(社群網站有其他方式,請自行參閱)。[7]
# 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。

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "^(https|imaps)://自己設定的client域名",
  "name" : "client1_server",
  "id" : 1,
  "evaluationOrder" : 1
}
  1. JWT Ticket設定: 要使用jwt as service ticket[5],務必要在build.gradle裡面加入套件,並且除了在application.properties設定後,service部分也需要設定。
  • 如果感覺一直吃不到設定檔的感覺,可以去把classes的資料夾,把他預設的application.properties檔案給砍了,只留下自己寫的即可。

  • 如果不知道jwt的signing key和encrpytion key要設定成什麼,可以先在不設定的情況下啟動一次Server,正常順利的話看看console,系統會自動產出一組給你,請把它複製起來貼在json裡面。

# JWT Tickets
cas.authn.token.crypto.encryptionEnabled=true
cas.authn.token.crypto.signingEnabled=true
  • 請把這段json code加在service.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...." ] ]
    }
  }
  1. Single Logout(SLO)設定: 這邊一樣非常重要! 如果這邊沒設定的話,即便社群有說SLO預設是啟動的,但沒設定要發送到client的哪個位子,基本上client也收不到,就做不了事情[8][9]
  • 這個設定預設是false,主要是設定如果網址後面帶service=xxxx,當server做完事情後,會導回client中你指定的url。(請參閱CAS Protocol 2.3.1)
# cas logout setting
cas.logout.followServiceRedirects=true
cas.slo.asynchronous=true
  • 一樣將下列這段json code加進service.json中,預設是back_channel,這種方式會直接讓server背地裡直接傳request給client,如果是front_channel,要登出前就會跳出視窗讓使用者確認。
"logoutType": "BACK_CHANNEL",
"logoutUrl": "看你想將它導到client的哪個頁面的網址"

Reference:

[1] JWT Service Ticket
[2] LDAP Authentication
[3] CAS Protocol
[4] CAS Overlay
[5] CAS Properties: JWT Ticket
[6] CAS Properties: LDAP Connection Pool v5.0
[7] CAS Properties: Service Registry
[8] CAS Properties: Single Logout
[9] Single Logout(SLO)

夜雨
由於一開始第一次接觸什麼叫Overlay的關係,一開始抓git抓CAS Server專案包下來的時候選了master版本,結果只看到抓下來一大堆各式各樣的xxx server,看得頭昏眼花又搞不太懂,很不像傳統一個網頁project的樣子重點是build又要超久!! 後來選了6.0版後,才終於看起來有比較像平常看到的那種專案結構了