本文提供一個Okta與阿里雲進行OIDC角色SSO的樣本,使Okta中的應用通過臨時身份憑證(STS Token)安全訪問阿里雲資源。
前提條件
請提前在Okta中註冊一個OIDC應用,並擷取應用的頒發者URL和用戶端ID(Client ID)。本樣本中使用的資料如下:- 頒發者URL:https://dev-xxxxxx.okta.com
- 用戶端ID:0oa294vi1vJoClev****
步驟一:在阿里雲建立OIDC身份供應商
本步驟中將建立一個名為TestOidcProvider
的OIDC身份供應商。頒發者URL為https://dev-xxxxxx.okta.com
,用戶端ID為0oa294vi1vJoClev****
。
- 使用阿里雲帳號登入RAM控制台。
- 在左側導覽列,選擇 。
- 在角色SSO頁簽,先單擊OIDC頁簽,然後單擊建立身份供應商。
- 在建立身份供應商頁面,設定身份供應商資訊。
參數 說明 身份供應商名稱 同一個阿里雲帳號下必須唯一。 頒發者URL 頒發者URL由外部IdP提供。頒發者URL必須以 https
開頭,符合標準URL格式,但不允許帶有query參數(以?
標識)、fragment片段(以#
標識)和登入資訊(以@
標識)。驗證指紋 為了防止頒發者URL被惡意劫持或篡改,您需要配置外部IdP的HTTPS CA認證產生的驗證指紋。阿里雲會輔助您自動計算該驗證指紋,但是建議您在本地自己計算一次(例如:使用OpenSSL計算指紋),與阿里雲計算的指紋進行對比。如果對比發現不同,則說明該頒發者URL可能已經受到攻擊,請您務必再次確認,並填寫正確的指紋。 用戶端ID 您的應用在外部IdP註冊的時候,會產生一個用戶端ID(Client ID)。當您從外部IdP申請簽發OIDC令牌時必須使用該用戶端ID,簽發出來的OIDC令牌也會通過 aud
欄位攜帶該用戶端ID。在建立OIDC身份供應商時配置該用戶端ID,然後在使用OIDC令牌換取STS Token時,阿里雲會校正OIDC令牌中aud
欄位所攜帶的用戶端ID與OIDC身份供應商中配置的用戶端ID是否一致。只有一致時,才允許扮演角色。如果您有多個應用需要訪問阿里雲,您可以配置多個用戶端ID,但最多不能超過20個。
備忘 身份供應商的描述資訊。 - 單擊確定。
步驟二:在阿里雲建立可信實體為OIDC身份供應商的RAM角色
本步驟中將建立一個名為testoidc
的RAM角色,身份供應商選擇步驟一建立的TestOidcProvider
。
使用Resource Access Management員登入RAM控制台。
在左側導覽列,選擇 。
在角色頁面,單擊建立角色。
在建立角色頁面,選擇可信實體類型為身份供應商,然後單擊下一步。
輸入角色名稱和備忘。
選擇身份供應商類型為OIDC。
選擇身份供應商並設定限制條件。
支援的限制條件如下表所示:
限制條件關鍵字
說明
是否必選
樣本
oidc:iss
OIDC頒發者(Issuer)。用來扮演角色的OIDC令牌中的iss欄位值必須滿足該限制條件要求,角色才允許被扮演。
該限定條件必須使用StringEquals作為條件操作類型,條件值只能是您在OIDC身份供應商中填寫的頒發者URL。該限制條件用於確保只有受信頒發者頒發的OIDC令牌才能扮演角色。
是
https://dev-xxxxxx.okta.com
oidc:aud
OIDC受眾(Audience)。用來扮演角色的OIDC令牌中的aud欄位值必須滿足該限制條件要求,角色才允許被扮演。
該限定條件必須使用StringEquals作為條件操作類型,您可選擇在OIDC身份供應商中配置的一個或多個用戶端ID(Client ID)作為條件值。該限制條件用於確保只有您設定的Client ID產生的OIDC令牌才能扮演角色。
是
0oa294vi1vJoClev****
oidc:sub
OIDC主體(Subject)。用來扮演角色的OIDC令牌中的sub欄位值必須滿足該限制條件要求時,角色才允許被扮演。
該限定條件可以使用任何String類的條件操作類型,且您可以最多設定10個OIDC主體作為條件值。該限制條件用於進一步限制允許扮演角色的身份主體,您也可以不指定該限制條件。
否
00u294e3mzNXt4Hi****
單擊完成。
單擊關閉。
步驟三:為RAM角色授權
您可以根據實際需要,為步驟二建立的RAM角色testoidc
授予訪問阿里雲資源的許可權。
使用Resource Access Management員登入RAM控制台。
在左側導覽列,選擇 。
在角色頁面,單擊目標RAM角色操作列的新增授權。
您也可以選中多個RAM角色,單擊角色列表下方的新增授權,為RAM角色大量授權。
在新增授權頁面,為RAM角色授權。
選擇授權範圍。
整個雲帳號:許可權在當前阿里雲帳號內生效。
指定資源群組:許可權在指定的資源群組內生效。
說明指定資源群組授權生效的前提是該雲端服務已支援資源群組。更多資訊,請參見支援資源群組的雲端服務。
指定授權主體。
授權主體即需要授權的RAM角色,系統會自動填入當前的RAM角色,您也可以添加其他RAM角色。
選擇權限原則。
說明每次最多綁定5條策略,如需綁定更多策略,請分次操作。
單擊確定。
單擊完成。
步驟四:在Okta簽發OIDC令牌(OIDC Token)
阿里雲不支援使用OIDC登入控制台,所以您需要使用程式訪問的方式完成OIDC SSO流程。由於產生OIDC Token本質上是個OAuth流程,所以您需要通過標準的OAuth 2.0流程從OIDC IdP(例如:Okta)擷取OIDC Token。OAuth支援多種流程,例如:比較常見的Authorization Code Flow。但由於該流程較為複雜,為示範方便,如下將以比較簡單的Implicit Flow為例,為您介紹擷取OIDC Token並最終完成SSO的流程,其中簡化了標準協議要求的部分步驟。
- 搭建一個用戶端Web應用,用於接收Okta頒發的OIDC Token。本樣本中,將提供一個使用Java Spring Boot和Thymeleaf搭建的極簡用戶端Web應用。在本機8080連接埠部署Web應用,綁定的localhost指向127.0.0.1,因此在本機通過瀏覽器訪問localhost:8080就可以訪問到該Web應用。相關的範例程式碼如下:
- 靜態頁面範例程式碼
按照OAuth 2.0協議要求Okta回調給用戶端Web應用的資訊是通過錨點(fragment)來傳遞的,您可以通過一個Web頁面,直接提取出錨點參數來擷取回調的OIDC Token。假設您製作了如下這個簡單的靜態頁面,直接進行參數透傳。該頁面的完整地址為
http://localhost:8080/accessTokenCallback
,也就是Okta應用配置的回調地址redirect_uri
。<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <script> window.onload = function () { let fragment = window.location.hash.substring(1); window.location.href = "/receiveAccessToken?" + fragment; }; </script> </head> </html>
- 類範例程式碼
建立一個類,作為上述靜態頁面的控制器。
package com.aliyun.oauthtest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class CallbackController { @RequestMapping("accessTokenCallback") public String callback() { return "accessTokenCallback"; } }
- 靜態頁面範例程式碼
- 登入Okta,向Okta申請簽發OIDC Token。您需要先登入Okta,然後基於步驟1搭建的用戶端Web應用,直接構造並訪問URL:
https://dev-xxxxxx.okta.com/oauth2/v1/authorize?client_id=0oa294vi1vJoClev****&scope=openid&response_type=token%20id_token&state=testState&nonce=a_unique_nonce_1&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2FaccessTokenCallback
參數含義如下:
client_id
:Okta中註冊的OIDC應用的用戶端ID。scope
:取值為openid
。response_type
:Implicit Flow流程中取值為token id_token
。state
:表示用戶端的目前狀態,可以指定任意值。nonce
:防止重放攻擊,可以指定任意值。redirect_uri
:接收access_token
或id_token
的回調地址,即步驟1中的用戶端Web應用的地址。
本樣本中已經預先登入了Okta,所以系統會根據使用者佈建的
redirect_uri
重新導向到回調地址。如下地址中的id_token
就是OIDC Token。HTTP/1.1 302 Found Location: http://localhost:8080/accessTokenCallback#id_token=eyJraWQiOiJ6OUV0e****&access_token=eyJraWQiOiJseEQ3R****&token_type=Bearer&expires_in=3600&scope=openid&state=testState
- 解析OIDC Token。
您可以對步驟2擷取的結果進行簡單地解析,將
header
和payload
展開。請求樣本:
package com.aliyun.oauthtest; import java.util.Base64; import java.util.Base64.Decoder; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ClientAppController { @RequestMapping(value = "/receiveAccessToken", method = {RequestMethod.POST, RequestMethod.GET}, produces = "application/json") public Map<String, Object> receiveAccessToken(@RequestParam("access_token") String accessToken, @RequestParam("id_token") String idToken, @RequestParam("token_type") String tokenType, @RequestParam("expires_in") Long expireTime, @RequestParam("scope") String scope, @RequestParam("state") String state) { Map<String, Object> result = new TreeMap<>(); result.put("access_token", accessToken); result.put("id_token", idToken); result.put("token_type", tokenType); result.put("expires_in", "" + expireTime); result.put("scope", scope); result.put("state", state); String[] jwt = idToken.split("\\."); Decoder decoder = Base64.getDecoder(); result.put(" id token jwt header", JSON.parse(new String(decoder.decode(jwt[0])))); result.put(" id token jwt payload", JSON.parse(new String(decoder.decode(jwt[1])))); result.put(" id token jwt signature", jwt[2]); return result; } }
返回樣本:
{ " id token jwt header": { "kid": "z9EtyT345d-JLIJo2-5ySDO27LG4FPeOotbwJPT****", "alg": "RS256" }, " id token jwt payload": { "at_hash": "KKsdN3prZWTvBEMn-g****", "sub": "00u294e3mzNXt4Hi****", "aud": "0oa294vi1vJoClev****", "ver": 1, "idp": "0oa294iehxjUCZIO****", "amr": [ "pwd" ], "auth_time": 1636373097, "iss": "https://dev-xxxxxx.okta.com", "exp": 1636377759, "iat": 1636374159, "nonce": "a_unique_nonce_1", "jti": "ID.lmSU5AD2iKLCVu6_KLMIr52dpCprncxW38v-NCA****" }, "id token jwt signature": "ZEJEGIv4Zoau63****", "access_token": "eyJraWQiOiJseEQ3R****", "expires_in": "3600", "id_token": "eyJraWQiOiJ6OUV0e****", "scope": "openid", "state": "testState", "token_type": "Bearer" }
步驟五:使用OIDC Token換取STS Token
您可以直接調用AssumeRoleWithOIDC API,使用從步驟四擷取的未解析的OIDC Token換取STS Token。
請求樣本:
public static void main(String[] args)
{
IAcsClient client = initialization();
String jwtToken = "eyJraWQiOiJ6OUV0e****"; //從Okta擷取的未解析的id_token。
AssumeRoleWithOIDCRequest request = new AssumeRoleWithOIDCRequest();
request.setDurationSeconds(3600L);
request.setOIDCProviderArn("acs:ram::113511544585****:oidc-provider/TestOidcProvider");
request.setOIDCToken(jwtToken);
request.setRoleArn("acs:ram::113511544585****:role/testoidc");
request.setRoleSessionName("TestOidcAssumedRoleSession");
try
{
AssumeRoleWithOIDCResponse resp = client.getAcsResponse(request);
System.out.println("success requestId: " + resp.getRequestId());
System.out.println("success assume role arn: " + resp.getAssumedRoleUser().getArn());
System.out.println("success sts credential accessKey id: " + resp.getCredentials().getAccessKeyId());
System.out.println("success sts credential accessKey secret: " + resp.getCredentials().getAccessKeySecret());
System.out.println("success resp: " + JSON.toJSONString(resp));
}
catch(ClientException | SystemException e)
{
e.printStackTrace();
}
}
返回樣本:
success requestId: 3D57EAD2-8723-1F26-B69C-F8707D8B565D
success assume role arn: acs:ram::113511544585****:role/testoidc/TestOidcAssumedRoleSession
success sts credential accessKey id: STS.NUgYrLnoC37mZZCNnAbez****
success sts credential accessKey secret: CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****
success resp:
{
"AssumedRoleUser":
{
"Arn": "acs:ram::113511544585****:role/testoidc/TestOidcAssumedRoleSession",
"AssumedRoleId": "33157794895460****:TestOidcAssumedRoleSession"
},
"Credentials":
{
"AccessKeyId": "STS.NUgYrLnoC37mZZCNnAbez****",
"AccessKeySecret": "CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****",
"Expiration": "2021-10-20T04:27:09Z",
"SecurityToken": "CAIShwJ1q6Ft5B2yfSjIr****"
},
"OIDCTokenInfo":
{
"ClientIds": "0oa294vi1vJoClev****",
"Issuer": "https://dev-xxxxxx.okta.com",
"Subject": "00u294e3mzNXt4Hi****"
},
"RequestId": "3D57EAD2-8723-1F26-B69C-F8707D8B565D"
}
其中Credentials
中的資訊即為STS Token。
步驟六:使用STS Token訪問阿里雲資源
使用從步驟五擷取的STS Token訪問有許可權的阿里雲資源。