跳至主要内容

2 篇文章 含有標籤「azure」

檢視所有標籤

如何在 Azure Entra ID 中設定 SSO

· 閱讀時間約 6 分鐘

在現代應用程式中,單一登入(Single Sign-On, SSO)是一個非常重要的功能,它可以讓使用者在多個應用程式之間無縫切換,而不需要重複登入。Azure Entra ID 是 Azure 的一個身份驗證服務,可以幫助開發者實現 SSO 功能。本文將介紹如何在 Azure Entra ID 中設定 SSO,並且整合到自己開發的應用程式中。

什麼是 Azure Entra ID​

Azure Entra ID 是 Azure 的一個身份驗證服務,提供了一個安全、可擴展的身份驗證解決方案,可以幫助開發者實現 SSO 功能。Azure Entra ID 支持多種身份驗證方式,包括密碼、多因素身份驗證、社交登入等。

必要準備​

  • Azure 訂閱帳戶

建立 Enterprise 應用程式​

  1. 登入 Azure 入口網站,選擇 Microsoft Entra ID,進入 Enterprise applications,點擊 Add application。

設定 Enterprise 應用程式​

  1. 點擊 Create your own application。
  2. 輸入應用程式名稱
  3. 選擇 Integrate any other application you don't find in the gallery (Non-gallery),後點擊 Create。

加入可以登入的使用者和群組​

  1. 進入建立好的 Enterprise 應用程式,點擊 Users and groups。
  2. 點擊 Add user,選擇可以登入的使用者或群組後儲存。

設定 SSO 模式​

  1. 進入 Enterprise 應用程式,點擊 Single sign-on。
  2. 選擇 SAML。

設定 SAML 基本設定​

  1. 輸入 Identifier (Entity ID)。建議可以使用 https://{tenant-name}.onmicrosoft.com/{app-name}。
  2. 輸入 Reply URL 作為登入後的返回網址後點擊 Save。

設定 SAML 可取得的使用者屬性​

  1. 輸入 claim name
  2. 選擇 claim 的來源,這裡選 Attribute。
  3. 選擇 claim 的值

SAML Metadata​

做到這裡幾乎就完成了,畫面上會顯示 SAML Metadata,這是我們後續要用到的資訊。

  1. SP Identifier (Entity ID)
  2. IdP Identifier (Entity ID)
  3. IdP Login URL
  4. IdP Logout URL
  5. X.509 Certificate

整合到應用程式​

這裡以 Python FastAPI 示範如何整合 Azure Entra ID 的 SSO 功能。

  1. 安裝 fastapi 和 python3-saml。
pip install "fastapi[standard]" python3-saml
  1. 程式碼如下:
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse

from onelogin.saml2.auth import OneLogin_Saml2_Auth

app = FastAPI()

# SAML Settings (Replace with your own settings)
SAML_PROVIDERS = {
"microsoft": {
"sp_entity_id": "<SP entity ID>",
"idp_entity_id": "<IdP entity ID>",
"idp_login_url": "<IdP login URL>",
"idp_logout_url": "<IdP logout URL>",
"idp_cert_filepath": "<IdP certificate filepath>"
}
}

SAML_EMAIL_CLAIM = "email"
SAML_USERNAME_CLAIM = "username"

# SAML Helper Functions
def init_saml_auth_settings(request:Request, provider:str, provider_setting: Dict):
sp_entity_id = provider_setting.get("sp_entity_id", None)
idp_entity_id = provider_setting.get("idp_entity_id", None)
idp_login_url = provider_setting.get("idp_login_url", None)
idp_logout_url = provider_setting.get("idp_logout_url", None)
idp_cert_filepath = provider_setting.get("idp_cert_filepath", None)

if not sp_entity_id:
error_text = f"SAML {provider} SP entity ID not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_entity_id:
error_text = f"SAML {provider} IdP entity ID not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_login_url:
error_text = f"SAML {provider} IdP login URL not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_logout_url:
error_text = f"SAML {provider} IdP logout URL not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not idp_cert_filepath:
error_text = f"SAML {provider} IdP certificate filename not found"
log.warning(error_text)
raise HTTPException(status_code=500, detail=error_text)

if not os.path.exists(idp_cert_filepath):
log.warning(f"SAML certificate not found: {idp_cert_filepath}")
raise HTTPException(status_code=500, detail="SAML certificate not found.")


# Load the SAML certificate, and remove the header and footer lines and newlines to get the raw certificate string
with open(idp_cert_filepath, 'rb') as f:
saml_cert = f.read().decode('utf-8').replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----','').replace('\n','').strip()

pototcal = "https" if request.url.scheme == "https" else "http"
host = request.headers.get("host", "localhost")

origin_host = f"{pototcal}://{host}"
sp_acs_url = f"{origin_host}/saml/{provider}/acs"
sp_sls_url = f"{origin_host}/saml/{provider}/logout"

SAML_SETTINGS = {
"strict": True,
"debug": True,
"sp": {
"entityId": sp_entity_id,
"assertionConsumerService": {
"url": sp_acs_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"singleLogoutService": {
"url": sp_sls_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
},
"idp": {
"entityId": idp_entity_id,
"singleSignOnService": {
"url": idp_login_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"singleLogoutService": {
"url": idp_logout_url,
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": saml_cert
}
}

return SAML_SETTINGS


def init_saml_auth(request_data: Dict, request: Request, provider:str, provider_setting: Dict):
"""
Initialize the SAML authentication object
"""
settings = init_saml_auth_settings(request, provider, provider_setting)
auth = OneLogin_Saml2_Auth(request_data, settings)
return auth

def get_saml_provider_setting(provider: str):
"""
Get the SAML provider setting
"""
if len(SAML_PROVIDERS.items()) == 0:
raise HTTPException(404)

provider_setting = SAML_PROVIDERS.get(provider, None)
if not provider_setting:
raise HTTPException(status_code=500, detail="Provider setting not found")
return provider_setting


# SAML Routes

@app.get("/saml/{provider}/login")
async def saml_login(provider: str, request: Request):
provider_setting = get_saml_provider_setting(provider)
req = {
'https': 'on' if request.url.scheme == 'https' else 'off',
'http_host': request.headers.get('host', ''),
'script_name': request.url.path,
'get_data': dict(request.query_params),
'post_data': await request.form()
}

auth = init_saml_auth(req, request, provider, provider_setting)
sso_url = auth.login()
return RedirectResponse(url=sso_url)

@app.post("/saml/{provider}/acs")
async def acs(provider: str, request: Request, response: Response):
provider_setting = get_saml_provider_setting(provider)
req = {
'https': 'on' if request.url.scheme == 'https' else 'off',
'http_host': request.headers.get('host', ''),
'script_name': request.url.path,
'get_data': dict(request.query_params),
'post_data': await request.form()
}

auth = init_saml_auth(req, request, provider, provider_setting)
auth.process_response()
errors = auth.get_errors()
error_reason = auth.get_last_error_reason()

if errors:
log.error(f"Errors: {errors}, Reason: {error_reason}")
raise HTTPException(status_code=401, detail=f"SAML Authentication Failed: {error_reason}")

if not auth.is_authenticated():
raise HTTPException(status_code=401, detail="Not authenticated")

attributes = auth.get_attributes()
name_id = auth.get_nameid()

email = attributes.get(SAML_EMAIL_CLAIM, [None])[0]
username = attributes.get(SAML_USERNAME_CLAIM, ["User"])[0]

if not email:
log.warning(f"SAML login callback failed, email is missing: {attributes}")d in attributes")

return {"email": email, "username": username, "name_id": name_id}

@app.get("/saml/{provider}/logout")
async def logout(provider: str, request: Request):
provider_setting = get_saml_provider_setting(provider)
req = {
'https': 'on' if request.url.scheme == 'https' else 'off',
'http_host': request.headers.get('host', ''),
'script_name': request.url.path,
'get_data': dict(request.query_params),
'post_data': await request.form()
}

auth = init_saml_auth(req, request, provider, provider_setting)
return RedirectResponse(url=auth.logout())

@app.get("/saml/{provider}/metadata")
async def metadata(provider: str, request: Request):
provider_setting = get_saml_provider_setting(provider)
saml_setting = init_saml_auth_settings(request, provider, provider_setting)
settings = OneLogin_Saml2_Settings(settings=saml_setting)
metadata = settings.get_sp_metadata()
return Response(content=metadata, media_type="text/xml")

參考資料​

透過 Azure API Management 打造可靠的 Azure OpenAI 服務

· 閱讀時間約 6 分鐘

問題​

雖然 Azure OpenAI 提供了很強大的服務,但它的一些使用限制,比如每分鐘的 Token 數量(TPM)和每分鐘的請求次數(RPM),對於一些需要大量使用 OpenAI 服務的應用程式來說,可能會造成瓶頸。

這些限制對一般使用者來說可能不太明顯,但對於大型系統卻是不能忽視的問題。

幸好,透過 Azure API Management 可以有效解決這些限制,提升應用程式的效能和穩定性。

  • 提升使用者體驗 (UX):透過控制請求速度,避免因超出限制而導致的服務中斷,提供更穩定且一致的服務。
  • 提高應用程式彈性:在遇到限制時,自動調整請求策略,確保應用程式可以持續運作。
  • 強化錯誤處理:透過 API 管理提供的錯誤處理機制,您可以有效地處理各種錯誤狀態碼,並採取相應的措施。
  • 優化模型選擇:您可以根據不同的需求選擇適當的 OpenAI 模型,並透過 API 管理進行有效管理。
  • 靈活配置 API 策略:您可以根據實際情況調整 API 策略,例如設定不同的請求速率限制、控制權限等。
  • 加強監控和記錄:透過 API 管理的監控功能,您可以追蹤 API 呼叫的流量、性能和錯誤狀況,並進行相關分析。

架構說明​

透過 APIM 來管理 Azure OpenAI 服務,當使用者發送請求時,APIM 會根據設定的策略來控制請求速率,並根據服務的狀態來調整請求策略,確保服務的穩定性和可靠性。

當主要 OpenAI 服務發生錯誤時,APIM 會自動切換到次要服務,以確保服務的持續運作。

圖片來源

必要準備​

準備 Azure OpenAI API 規格文件​

首先我們需要先從官方 Github 中找到 Azure OpenAI API 的 OpenAPI 規格文件,並且下載到本機。 下載完成後,去掉 servers 這個欄位

{
"openapi": "3.0.0",
"info": {
"title": "Azure OpenAI Service API",
"description": "Azure OpenAI APIs for completions and search",
"version": "2024-02-01"
},
"security": [
{
"bearer": ["api.read"]
},
{
"apiKey": []
}
],
...
}

註冊多個 Azure OpenAI 服務到 Azure API Management Backends​

  1. 進入 Azure API Management 服務,從側邊欄點擊 Backends。
  2. 點擊 Add
  3. 填入
    • Name: Backend 名稱
    • Type: Custom URL
    • Runtime URL: https://{aoaiHostname}.openai.azure.com/openai
  4. 點擊 Advanced,勾選 Validate certificate chain 和 Validate certificate name
  5. 在 Authorization credentials 的 Headers 新增
    • Key: api-key
    • Value: Azure OpenAI 服務的 API 金鑰,可以使用 APIM 的 Named value 來存取
  6. 點擊 Create

上傳規格文件到 Azure API Management 建立 API​

  1. 進入 Azure API Management 服務,從側邊欄點擊 API。
  2. 點擊 OpenAPI Specification。
  3. 點擊 Add a new OpenAPI specification,然後選擇 File,選擇剛剛下載的 OpenAPI 規格文件。
  4. 填入 Display name 和 Name。
  5. 在 API URL suffix 填入 openai。
  6. 點擊 Create.

建立 API Policy​

  1. API 建立完成後,點擊剛剛建好的 API。
  2. 點擊 All operation。
  3. 點擊 Policies </> 按鈕。
  4. 複製以下範例,並貼上到編輯器中後儲存。
<policies>
<inbound>
<base />
<!-- default values if wasn't specified by the caller -->
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<!-- 節流方案 https://learn.microsoft.com/zh-tw/azure/api-management/api-management-sample-flexible-throttling -->

<!-- 1. 限速着重於短時間內(例如每分鐘)的請求頻率,旨在防止短時間內過度使用資源。 -->
<!-- 2. 配額則關注於長時間內(例如每天、每月)的總請求量,確保長期內資源的合理分配與使用。 -->
<!-- By Ip 限制請求頻率 https://learn.microsoft.com/en-us/azure/api-management/rate-limit-by-key-policy -->
<!-- 範例中的 calls 屬性設定為 10,表示每個 IP 在 60 秒內最多可以發出 10 個請求。 -->
<rate-limit-by-key calls="10" renewal-period="60" counter-key="@(context.Request.IpAddress)" />

<!-- By Ip 限制使用量配額 https://learn.microsoft.com/zh-tw/azure/api-management/quota-by-key-policy -->
<!-- 範例中的 calls 屬性設定為 100,表示每個 IP 在 3600 秒內最多可以發出 100 個請求。 -->
<quota-by-key calls="100" renewal-period="3600" counter-key="@(context.Request.IpAddress)" increment-condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 400)" first-period-start="2024-01-01T00:00:00Z" />

<!-- By Ip 限制 TPM https://learn.microsoft.com/zh-tw/azure/api-management/azure-openai-token-limit-policy -->
<!-- 範例中的 tokens-per-minute 屬性設定為 5000,表示每個 IP 在每分鐘內最多可以發出 5000 token。 -->
<azure-openai-token-limit counter-key="@(context.Request.IpAddress)" tokens-per-minute="5000" estimate-prompt-tokens="false" remaining-tokens-header-name="remaining-tokens" tokens-consumed-header-name="consumed-tokens" />


<!-- 預設 backend service 名稱為 primary -->
<set-backend-service backend-id="primary" />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode >= 500)" count="5" interval="1" delta="1" max-interval="8" first-fast-retry="false">
<!-- Failover logic below - uncomment to retry on secondary backend -->
<choose>
<!-- 若發生 aoai service 發生錯誤時,會呼叫另一台名稱為 secondary 的 backend service -->
<when condition="@(context.Response.StatusCode == 429 || context.Response.StatusCode >= 500)">
<set-backend-service backend-id="secondary" />
</when>
</choose>
<forward-request buffer-request-body="true" />
</retry>
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>

將 API 加入 Product​

  1. 點擊 Products。
  2. 點擊 Add。
  3. 填寫必填欄位。
  4. 加入剛剛建立的 API。
  5. 點擊 Create.

測試 API​

  1. 點擊側邊欄的 Subscription。
  2. 找到剛剛建立的 Product,並且複製 Key。
  3. 可以使用 Postman 或者其他工具來測試 API,下面範例使用 Python OpenAI SDK 來測試。
from openai import AzureOpenAI

apim_endpoint = "APIM 的 Gateway URL"
apim_product_name = "APIM 有註冊 OpenAI API 的 Product Name"
apim_product_subscription_key = "APIM 有註冊 OpenAI API 的 Product Key"
aoai_api_version = "OpenAI 的 API Version"

# 透過 Azure OpenAI SDK 來呼叫 APIM API
# 從 Azure OpenAI SDK 帶入 Product Key
# APIM 會從 Backend 服務選擇適當的服務,並帶入 API Key,處理請求
aoai = AzureOpenAI(
azure_endpoint=apim_endpoint,
api_key=apim_product_subscription_key,
api_version=aoai_api_version,
default_headers={"Ocp-Apim-Subscription-Key": f"{apim_product_subscription_key};product={apim_product_name}"},
)

messages = [{
"role": "user",
"content": "What are you?"
}]
model = "gpt-35-turbo"

response = aoai.chat.completions.create(
messages=messages,
model=model
)

參考資料​