CAS 6.x + Delegated Authentication SAML2.0 設定記錄

2023-03-19 21:00:28

最近領導派了一個活兒, 需要把我們CAS系統的身份識別交給甲方的系統, 甲方的系統是SAML2.0的協定。

由於之前對SAML2.0協定了解不多,折騰了不少時間,在這裡記錄一下。以後忘掉還可以看看。

首先是關於SAML2.0協定的簡單介紹,在B站找到一個還不錯的視訊。https://www.bilibili.com/video/BV1sA411Y758/

 

1.設定cas.properties,讓cas作為 SAML2.0 的 IDP(身份提供方)

  這一步其實沒啥用, 因為我是要對接別人的IDP 而不是讓自己的cas成為IDP,如果需要自己建一個cas作為IDP的話可以參考

cas.properties:  cas.authn.saml-idp.core.entity-id=${cas.server.name}/idp

build.gradle: implementation "org.apereo.cas:cas-server-support-saml-idp:${project.'cas.version'}"
設定完之後可以開啟idp路徑檢視idp/metadata-> http://localhost:8888/cas/idp/metadata

  上面兩個設定完成後其實作為IDP還是不能運作的(未授權的服務),你要對外提供身份驗證的話,除了別人設定你作為IDP, 你也需要知道是哪個服務在用你, 你還需要給SP(服務提供方)許可權,並且設定sp的metadata

 在/etc/cas/services下面設定sp的資訊,測試環境的話可以設定得粗暴一點:

saml2-10004.json:
{
  "@class" : "org.apereo.cas.support.saml.services.SamlRegisteredService",
  "serviceId" : ".+",
  "name" : "SAMLService",
  "id" : 10000003,
  "evaluationOrder" : 10,
  "metadataLocation" : "file:/etc/cas/config/sp-metadata-local.xml" 
}
View Code

 sp-metadata-local.xml: 這個檔案一般是sp提供的。裡面一般包含EntityID, 簽名和加密的共鑰,以及支援的NameID格式。需要的屬性欄位(這裡沒有配屬性欄位)。

<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor ID="_cb7748cb6778495398727ae8bfcaa72e62beea3" entityID="liveramp-sp"
                     validUntil="2043-03-16T11:36:25.739Z" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
    <md:Extensions xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport">
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha384"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha512"/>
        <alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>
        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
    </md:Extensions>
    <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false"
                        protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
        <md:Extensions xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init">
            <init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init"
                                   Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT"/>
        </md:Extensions>
        <md:KeyDescriptor use="signing">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>xxxx
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:KeyDescriptor use="encryption">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>xxxx
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                                Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
                                Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
                                Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
                                Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                                     Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT"
                                     index="0"/>
    </md:SPSSODescriptor>
</md:EntityDescriptor>
View Code

  如果裡面含有屬性欄位, sp-metadata.xml會多出一些內容:

        <md:AttributeConsumingService index="0">
            <md:RequestedAttribute Name="firstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
                                   isRequired="false"/>
            <md:RequestedAttribute Name="lastName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
                                   isRequired="false"/>
            <md:RequestedAttribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
                                   isRequired="false"/>
            <md:RequestedAttribute FriendlyName="displayName" Name="urn:oid:2.16.840.1.113730.3.1.241"
                                   NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false"/>
            <md:RequestedAttribute FriendlyName="cn" Name="cn"
                                   NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false"/>
        </md:AttributeConsumingService>
View Code

 

2. 設定委託認證

  1). cas的委託認證都是通過pac4j去做的, 所以先要加上pac4j的依賴。

   build.gradle里加上依賴:  implementation "org.apereo.cas:cas-server-support-pac4j-webflow:${project.'cas.version'}" 

  2) 修改cas.properties加上委託認證的設定

cas.authn.pac4j.core.lazy-init=false
cas.authn.pac4j.saml[0].client-name=SAML2.0-CLIENT
cas.authn.pac4j.saml[0].display-name=SAML

# 如果是檔案 file:/xxx/xxx.xml
cas.authn.pac4j.saml[0].identity-provider-metadata-path=https://yd-dev.liverampapac.com/sso/idp/metadata


cas.authn.pac4j.saml[0].keystore-path=file:/private/etc/cas/config/saml2.0/cer/samlKeystore.jks
cas.authn.pac4j.saml[0].keystore-password=xxx
cas.authn.pac4j.saml[0].private-key-password=xxx
cas.authn.pac4j.saml[0].service-provider-entity-id=xxxxx
# this is important
cas.authn.pac4j.saml[0].service-provider-metadata-path=file:/etc/cas/config/saml2.0/sp-metadata.xml
# If false global logout may not work, it's depends on IDP settings
cas.authn.pac4j.saml[0].sign-service-provider-logout-request=true
View Code

  使用委託認證就是要把認證的行為交給IDP, 而自己成為SP. IDP跟SP之間的通訊的安全,或者說他們之前的信任是需要證書支援的,一般在IDP和SP的metadata都會設定證書的公鑰,當資訊傳到自己系統時, 用自己的私鑰做驗籤/解密。

  上面在cas.properties的改動裡還有一個 sp-metadata.xml需要我們自己構建(下面這個sp-metadata.xml 最低設定其實只需要配兩個公鑰就能工作了,其它的cas會幫我們自動生成)

<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor ID="aabc3592a94845c6b38b88894e6d19415746a23"
                     entityID="liveramp-sp"
                     xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
    <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false"
                        protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
        <md:KeyDescriptor use="signing">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>xxx
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:KeyDescriptor use="encryption">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>xxx
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>

        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT&amp;logoutendpoint=true"/>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</md:NameIDFormat>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT" index="0"/>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="http://localhost:8888/cas/login?client_name=IHG-SAML2.0-CLIENT" index="1"/>


<!--   設定在這裡 sp/metadata並不會展示和實際起作用 需要在cas.properties中設定     -->
        <md:AttributeConsumingService index="0"
                                      xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
                                      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
            <!-- SAML V2.0 attribute syntax -->
            <md:RequestedAttribute isRequired="true"
                                   NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
                                   Name="cn"
                                   FriendlyName="cn"/>
            <md:RequestedAttribute
                    NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
                    Name="urn:oid:2.16.840.1.113730.3.1.241"
                    FriendlyName="displayName"/>
            <md:RequestedAttribute Name="firstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
            <md:RequestedAttribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
            <md:RequestedAttribute Name="lastName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
        </md:AttributeConsumingService>
    </md:SPSSODescriptor>
</md:EntityDescriptor>
View Code

  證書的生成如果覺得麻煩的話, 這裡有一鍵生成工具,兩個檔案放一個目錄下面 然後一鍵生成 ./gen.sh [*name] [*domain] [*passwd] ,生成出來把crt檔案的內容放到<X509Certificate>裡面。jks檔案路徑和passwd也都需要設定到

cas.porperties裡面.

  gen.sh:

#!/bin/bash
echo "自動建立自定義證書"
region=$1
region_suffix=$2
pass=$3

if [[ "X$region" == "X" ]]; then
  echo "region is empty"
  exit 1
fi

if [[ "X$region_suffix" == "X" ]]; then
  echo "region_suffix is empty"
  exit 1
fi

if [[ ! -f "openssl.cnf" ]]; then
  echo "openssl.cnf file is not exist"
  exit 1
fi

mkdir $region

cp openssl.cnf $region/

cd $region

mkdir demoCA

cd demoCA

mkdir private

SAN="[SAN]\nsubjectAltName=DNS:*.$region_suffix"
subj="/C=CN/ST=NT/L=NT/O=LIVERAMP-CN/OU=TS/CN=$region_suffix"

echo "生成CA根證書"
openssl genrsa -des3 -passout pass:$pass -out private/cakey.pem 2048
openssl req -sha256 -new -nodes -key private/cakey.pem -passin pass:$pass \
  -x509 -days 3650 -out cacert.pem -subj "$subj"

cd ..

echo "生成中間CA證書"
openssl genrsa -des3 -passout pass:$pass -out $region.key 2048

openssl req -new -key $region.key \
  -passin pass:$pass \
  -subj "$subj" -reqexts SAN \
  -config <(cat openssl.cnf \
    <(printf "$SAN")) \
  -out $region.csr

echo "移除密碼"
cp $region.key $region.key.org
openssl rsa -passin pass:$pass -in $region.key.org -out $region.key

echo "生成x509 crt證書"
openssl x509 -req \
  -days 36500 \
  -in $region.csr \
  -signkey $region.key \
  -out $region.crt \
  -extensions SAN \
  -extfile <(cat openssl.cnf <(printf "$SAN"))

echo "檢視crt證書"
openssl x509 -text -in $region.crt
echo "生成pkcs12"
openssl pkcs12 -passout pass:$pass -export -in $region.crt -inkey $region.key -out $region.p12 -CAfile $region.crt
echo "生成JKS"
keytool -importkeystore -v -srckeystore $region.p12 -srcstoretype pkcs12 -srcstorepass $pass -destkeystore $region.jks -deststoretype pkcs12 -deststorepass $pass
gen.sh
HOME            = .
oid_section     = new_oids
[ new_oids ]
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7

[ ca ]
default_ca  = CA_default        

[ CA_default ]
dir     = ./demoCA      
certs       = $dir/certs        
crl_dir     = $dir/crl      
database    = $dir/index.txt
new_certs_dir   = $dir/newcerts
certificate = $dir/cacert.pem   
serial      = $dir/serial       
crlnumber   = $dir/crlnumber
crl     = $dir/crl.pem      
private_key = $dir/private/cakey.pem
x509_extensions = usr_cert
name_opt    = ca_default        
cert_opt    = ca_default
default_days    = 365           
default_crl_days= 30            
default_md  = default       
preserve    = no
policy      = policy_match

[ policy_match ]
countryName     = match
stateOrProvinceName = match
organizationName    = match
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ policy_anything ]
countryName     = optional
stateOrProvinceName = optional
localityName        = optional
organizationName    = optional
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

[ req ]
default_bits        = 2048
default_keyfile     = privkey.pem
distinguished_name  = req_distinguished_name
attributes      = req_attributes
x509_extensions = v3_ca
string_mask = utf8only

[ req_distinguished_name ]
countryName         = Country Name (2 letter code)
countryName_default     = AU
countryName_min         = 2
countryName_max         = 2
stateOrProvinceName     = State or Province Name (full name)
stateOrProvinceName_default = Some-State
localityName            = Locality Name (eg, city)
0.organizationName      = Organization Name (eg, company)
0.organizationName_default  = Internet Widgits Pty Ltd
organizationalUnitName      = Organizational Unit Name (eg, section)
commonName          = Common Name (e.g. server FQDN or YOUR name)
commonName_max          = 64
emailAddress            = Email Address
emailAddress_max        = 64

[ req_attributes ]
challengePassword       = A challenge password
challengePassword_min       = 4
challengePassword_max       = 20
unstructuredName        = An optional company name

[ usr_cert ]
basicConstraints=CA:FALSE
nsComment           = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,CA:true

[ crl_ext ]
authorityKeyIdentifier=keyid:always

[ proxy_cert_ext ]
basicConstraints=CA:FALSE
nsComment           = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo

[ tsa ]
default_tsa = tsa_config1   

[ tsa_config1 ]
dir     = ./demoCA      
serial      = $dir/tsaserial    
crypto_device   = builtin       
signer_cert = $dir/tsacert.pem
certs       = $dir/cacert.pem
signer_key  = $dir/private/tsakey.pem 
signer_digest  = sha256         
default_policy  = tsa_policy1
other_policies  = tsa_policy2, tsa_policy3  
digests     = sha1, sha256, sha384, sha512  
accuracy    = secs:1, millisecs:500, microsecs:100  
clock_precision_digits  = 0 
ordering        = yes
tsa_name        = yes
ess_cert_id_chain   = no
ess_cert_id_alg     = sha1
openssl.cnf

  設定完這些,順利的話可以在 http://localhost:8888/cas/sp/metadata 看到sp的後設資料。 然後將這個後設資料提供給IDP。

 接下來在cas登入的時候, 應該可以從右側看到IDP的名字,點一下試試能不能登入。如果不能的話, 說明設定還有一些問題。

  3. 屬性釋放 Attribute Release (IDP需要設定),這個是為了本地測試設定的, 實際上如果IDP那邊設定好了,是不用管這個的

  作為IDP,當有SP登入成功時, 你需要確定將登入者的哪些屬性給到SP, 可以設定services/xx.json 新增以下設定:

 "attributeReleasePolicy": {
        "@class": "org.apereo.cas.support.saml.services.PatternMatchingEntityIdAttributeReleasePolicy",
        "allowedAttributes" : [ "java.util.ArrayList", [ "cn","sn","email","firstName","lastName"]],
        "fullMatch" : "true",
        "reverseMatch" : "false",
        "entityIds" : ".+"
  }

 

4. 屬性請求(SP需要設定)

  當SP向IDP請求登入時, 希望IDP返回登入者的一些資訊比如email之類的。 設定cas.properties

    cas.authn.pac4j.saml[0].attribute-consuming-service-index=0
    cas.authn.pac4j.saml[0].requested-attributes[0].name=FirstName
    cas.authn.pac4j.saml[0].requested-attributes[0].nameFormat=urn:oasis:names:tc:SAML:2.0:attrname-format:basic
    cas.authn.pac4j.saml[0].requested-attributes[1].name=LastName
    cas.authn.pac4j.saml[0].requested-attributes[1].nameFormat=urn:oasis:names:tc:SAML:2.0:attrname-format:basic
    cas.authn.pac4j.saml[0].requested-attributes[2].name=email
    cas.authn.pac4j.saml[0].requested-attributes[2].nameFormat=urn:oasis:names:tc:SAML:2.0:attrname-format:basic
    #eduPersonNickname urn:oid:2.16.840.1.113730.3.1.241
    #cas.authn.pac4j.saml[0].requested-attributes[3].name=urn:oid:2.16.840.1.113730.3.1.241
    #cas.authn.pac4j.saml[0].requested-attributes[3].nameFormat=urn:oasis:names:tc:SAML:2.0:attrname-format:uri
    #cas.authn.pac4j.saml[0].requested-attributes[3].friendlyName=displayName
    #cas.authn.pac4j.saml[0].requested-attributes[4].name=cn
    #cas.authn.pac4j.saml[0].requested-attributes[4].nameFormat=urn:oasis:names:tc:SAML:2.0:attrname-format:uri
    #cas.authn.pac4j.saml[0].requested-attributes[4].friendlyName=cn