

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

# DICOMweb APIs の OIDC 認証
<a name="dicomweb-oidc"></a>

AWS HealthImaging は、既存の Signature Version 4 [(SigV4) 認証に加えて、OpenID Connect (OIDC](https://openid.net/specs/openid-connect-core-1_0.html)) を使用した DICOMweb API リクエストの [OAuth 2.0](https://oauth.net/2/) ベースの認証をサポートしています。 [AWS SigV4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) OIDC を使用すると、HealthImaging を外部 ID プロバイダー (IdPs) と直接統合でき、各アプリケーションが AWS 認証情報を持つことなく、HealthImaging DICOMweb エンドポイントを介して標準ベースのアプリケーションに医療画像データへのアクセスを提供できます。

**Topics**
+ [Lambda オーソライザーによるカスタムトークン検証](dicomweb-oidc-how.md)
+ [OIDC 認証用の AWS Lambda オーソライザーを設定する](dicomweb-oidc-requirements.md)

# Lambda オーソライザーによるカスタムトークン検証
<a name="dicomweb-oidc-how"></a>

HealthImaging は、Lambda オーソライザーを使用するアーキテクチャを通じて OIDC サポートを実装し、お客様が独自のトークン検証ロジックを実装できるようにします。この設計により、OIDC 互換 ID プロバイダー (IdPs) の多様な環境とさまざまなトークン検証方法に対応して、トークンの検証方法とアクセス決定の適用方法を柔軟に制御できます。

## 認証のフロー
<a name="dicomweb-oidc-authentication-flow"></a>

認証の仕組みの概要は次のとおりです。

1. **クライアントが DICOMweb API を呼び出す:** アプリケーションは選択した OIDC ID プロバイダーで認証し、署名付き ID トークン (JWT) を受け取ります。DICOMweb HTTP リクエストごとに、クライアントは認可ヘッダー (通常はベアラートークン) に OIDC アクセストークンを含める必要があります。リクエストがデータに到達する前に、HealthImaging は受信リクエストからこのトークンを抽出し、設定した Lambda オーソライザーを呼び出します。

   1. ヘッダーは通常、 の形式に従います`Authorization: Bearer <token>`。

1. **初期検証:** HealthImaging は、Lambda 関数を不必要に呼び出すことなく、明らかに無効または期限切れのトークンをすばやく拒否するために、アクセストークンクレームを検証します。HealthImaging は、Lambda オーソライザーを呼び出す前に、アクセストークン内の特定の標準クレームの初期検証を実行します。

   1. `iat` (発行時): HealthImaging は、トークンの発行時間が許容範囲内であるかどうかをチェックします。

   1. `exp` (有効期限): HealthImaging はトークンの有効期限が切れていないことを確認します。

   1. `nbf` (時間前ではない): 存在する場合、HealthImaging は有効な開始時刻より前にトークンが使用されていないことを確認します。

1. **HealthImaging は Lambda オーソライザーを呼び出します。**最初のクレーム検証に合格すると、HealthImaging は追加のトークン検証をお客様が設定した Lambda オーソライザー関数に委任します。HealthImaging は、抽出されたトークンおよびその他の関連するリクエスト情報を Lambda 関数に渡します。Lambda 関数は、トークンの署名とクレームを検証します。

1. **ID プロバイダーとの検証:** Lambda には、ID トークンの署名をチェックし、より広範なトークン検証 (発行者、対象者、カスタムクレームなど) を実行し、必要に応じてそれらのクレームを IdP に対して検証するカスタムコードが含まれています。

1. オー**ソライザーはアクセスポリシーを返します。**検証に成功すると、Lambda 関数は認証された使用に対する適切なアクセス許可を決定します。次に、Lambda オーソライザーは、付与される一連のアクセス許可を表す IAM ロールの amazon リソースネーム (ARN) を返します。

1. **リクエストの実行:** 引き受けた IAM ロールに必要なアクセス許可がある場合、HealthImaging はリクエストされた DICOMWeb リソースの返却に進みます。アクセス許可が不十分な場合、HealthImaging はリクエストを拒否し、適切なエラーレスポンスエラー (403 Forbidden) を返します。

**注記**  
AWS HealthImaging サービスによって管理されていないオーソライザー Lambda 関数。 AWS アカウントで実行されます。お客様は、関数の呼び出しと実行時間に対して、HealthImaging 料金とは別に課金されます。

## アーキテクチャの概要
<a name="dicomweb-oidc-architecture-overview"></a>

![\[ワークフローを示す図: クライアントがトークンを送信、Lambda オーソライザーが検証、HealthImaging プロセスリクエスト\]](http://docs.aws.amazon.com/ja_jp/healthimaging/latest/devguide/images/security-oidc-workflow-lambda.png)


## 前提条件
<a name="dicomweb-oidc-prerequisites"></a>

### アクセストークンの要件
<a name="dicomweb-oidc-token-requirements"></a>

HealthImaging では、アクセストークンが JSON Web Token (JWT) 形式である必要があります。多くの ID プロバイダー (IDPs ではアクセストークンフォームを選択または設定できます。統合を進める前に、選択した IDP が JWT トークンを発行できることを確認します。

トークンの形式  
アクセストークンは JWT (JSON Web Token) 形式である必要があります

必要な請求    
`exp` (有効期限)  
トークンがいつ無効になるかを指定する必須クレーム。  
+ UTC の現在の時刻より後である必要があります
+ トークンが無効になったときを表します  
`iat` (発行先)  
トークンの発行日時を指定する必須クレーム。  
+ UTC の現在の時刻より前である必要があります
+ UTC の現在の時刻より 12 時間前にすることはできません
+ これにより、トークンの最大有効期間は 12 時間になります。  
`nbf` (以前ではありません)  
トークンを使用できる最も早い時刻を指定するオプションクレーム。  
+ 存在する場合、HealthImaging によって評価されます
+ トークンを受け入れない時間を指定します。

### Lambda オーソライザーの応答時間の要件
<a name="dicomweb-oidc-lambda"></a>

HealthImaging は、最適な API パフォーマンスを確保するために、Lambda オーソライザーレスポンスに厳格なタイミング要件を適用します。Lambda 関数は 1 秒以内に を返す**必要があります**。

## ベストプラクティス
<a name="dicomweb-oidc-best-practices"></a>

### トークン検証の最適化
<a name="dicomweb-oidc-optimization"></a>
+ 可能であれば JWKS (JSON ウェブキーセット) をキャッシュする
+ 可能な場合は有効なアクセストークンをキャッシュする
+ ID プロバイダーへのネットワーク呼び出しを最小限に抑える
+ 効率的なトークン検証ロジックを実装する

### Lambda 設定
<a name="dicomweb-oidc-lambda-configuration"></a>
+ Python および Node.js ベースの関数は通常、より高速に初期化します。
+ ロードする外部ライブラリの量を減らす
+ 一貫したパフォーマンスを確保するために適切なメモリ割り当てを設定する
+ CloudWatch メトリクスを使用して実行時間をモニタリングする

## OIDC 認証の有効化
<a name="dicomweb-oidc-enablement"></a>
+ OIDC 認証****は、**新しい**データストアを作成する場合にのみ有効にできます
+ 既存のデータストアの OIDC の有効化は API ではサポートされていません
+ 既存のデータストアで OIDC を有効にするには、 サポートに連絡する必要があります AWS 。

# OIDC 認証用の AWS Lambda オーソライザーを設定する
<a name="dicomweb-oidc-requirements"></a>

このガイドでは、選択した ID プロバイダー (IdP) が HealthImaging OIDC 認証機能の要件と互換性のあるアクセストークンを提供するように設定されていることを前提としています。

## 1. DICOMWeb API アクセス用の IAM ロールを設定する
<a name="dicomweb-oidc-iam-roles"></a>

Lambda オーソライザーを設定する前に、DICOMWeb API リクエストを処理するときに HealthImaging が引き受ける IAM ロールを作成します。オーソライザー Lambda 関数は、トークンの検証に成功した後、これらのロール ARN のいずれかを返します。これにより、HealthImaging は適切なアクセス許可でリクエストを実行できます。

1. 目的の DICOMWeb API 権限を定義する IAM ポリシーを作成します。利用可能なアクセス許可については、HealthImaging ドキュメントの[DICOMweb の使用](https://docs.aws.amazon.com/healthimaging/latest/devguide/using-dicomweb.html)」セクションを参照してください。

1. 次の IAM ロールを作成します。
   + これらのポリシーをアタッチする
   + AWS HealthImaging サービスプリンシパル (`medical-imaging.amazonaws.com`) がこれらのロールを引き受けることを許可する信頼関係を含めます。

以下は、関連付けられたロールが HealthImaging DICOMWeb 読み取り専用 API にアクセスすることを許可するポリシーの例です。

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Sid": "MedicalImagingDicomWebOperations",
            "Effect": "Allow",
            "Action": [
                "medical-imaging:SearchDICOMInstances",
                "medical-imaging:GetImageSetMetadata",
                "medical-imaging:GetDICOMSeriesMetadata",
                "medical-imaging:SearchDICOMStudies",
                "medical-imaging:GetDICOMBulkdata",
                "medical-imaging:SearchDICOMSeries",
                "medical-imaging:GetDICOMInstanceMetadata",
                "medical-imaging:GetDICOMInstance",
                "medical-imaging:GetDICOMInstanceFrames"
            ],
            "Resource": "arn:aws:medical-imaging:us-east-1:123456789012:datastore/datastore-123"
        }
    ]
}
```

------

ロール (複数可) に関連付ける必要がある信頼関係ポリシーの例を次に示します。

------
#### [ JSON ]

****  

```
{
    "Version":"2012-10-17",		 	 	 
    "Statement": [
        {
            "Sid": "OIDCRoleFederation",
            "Effect": "Allow",
            "Principal": {
                "Service": "medical-imaging.amazonaws.com"
        },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

------

次のステップで作成する Lambda オーソライザーは、トークンクレームを評価し、適切なロールの ARN を返すことができます。その後、AWS HealthImaging はこのロールを偽装して、対応するアクセス許可で DICOMWeb API リクエストを実行します。

例えば、次のようになります。
+ 「管理者」クレームを持つトークンは、フルアクセスを持つロールの ARN を返す場合があります
+ 「リーダー」クレームを持つトークンは、読み取り専用アクセスを持つロールの ARN を返す場合があります
+ 「department\$1A」クレームを持つトークンは、その部門のアクセスレベルに固有のロールの ARN を返す場合があります

このメカニズムにより、IAM ロールを介して IdP の認可モデルを特定の AWS HealthImaging アクセス許可にマッピングできます。

## 2. Lambda オーソライザー関数の作成と設定
<a name="dicomweb-oidc-configure-lambda"></a>

JWT トークンを検証し、トークンクレーム評価に基づいて適切な IAM ロール ARN を返す Lambda 関数を作成します。この関数はヘルスイメージングサービスによって呼び出され、HealthImaging データストア ID、DICOMWeb オペレーション、HTTP リクエストで見つかったアクセストークンを含むイベントを渡します。

```
{
  "datastoreId": "{datastore id}",
  "operation": "{Healthimaging API name e.g. GetDICOMInstance}",
  "bearerToken": "{access token}"
}
```

Lambda オーソライザー関数は、次の構造を持つ JSON レスポンスを返す必要があります。

```
{
  "isTokenValid": {true or false},
  "roleArn": "{role arn or empty string meaning to deny the request explicitly}"
}
```

詳細については、実装例を参照してください。

**注記**  
DICOMWeb リクエストは、Lambda オーソライザーによってアクセストークンが検証された後にのみ応答されるため、この関数の実行を可能な限り速くして、最適な DICOMWeb API 応答時間を提供することが重要です。

HealthImaging サービスが Lambda オーソライザー関数を呼び出す権限を付与されるようにするには、HealthImaging サービスが関数を呼び出すことを許可するリソースポリシーが必要です。このリソースポリシーは、Lambda 設定タブのアクセス許可メニューまたは以下を使用して作成できます AWS CLI。

```
aws lambda add-permission \
    --function-name YourAuthorizerFunctionName \
    --statement-id HealthImagingInvoke \
    --action lambda:InvokeFunction \
    --principal medical-imaging.amazonaws.com
```

このリソースポリシーにより、HealthImaging サービスは DICOMWeb API リクエストを認証するときに Lambda オーソライザーを呼び出すことができます。

**注記**  
Lambda リソースポリシーは、特定の HealthImaging データストアの ARN に一致するArnLike」条件を使用して後で更新できます。

Lambda リソースポリシーの例を次に示します。

------
#### [ JSON ]

****  

```
{
  "Version":"2012-10-17",		 	 	 
  "Id": "default",
  "Statement": [
    {
      "Sid": "LambaAuthorizer-HealthImagingInvokePermission",
      "Effect": "Allow",
      "Principal": {
        "Service": "medical-imaging.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:123456789012::function:{LambdaAuthorizerFunctionName}",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:medical-imaging:us-east-1:123456789012:datastore/datastore-123"
        }
      }
    }
  ]
}
```

------

## 3. OIDC 認証を使用して新しいデータストアを作成する
<a name="dicomweb-oidc-datastore"></a>

OIDC 認証を有効にするには、lambda-authorizer-arn」パラメータ AWS CLI を持つ を使用して新しいデータストアを作成する必要があります。サポートに連絡して AWS いなければ、既存のデータストアで OIDC 認証を有効にすることはできません。

OIDC 認証を有効にして新しいデータストアを作成する方法の例を次に示します。

```
aws medical-imaging create-datastore \
    --datastore-name YourDatastoreName \
    --lambda-authorizer-arn YourAuthorizerFunctionArn
```

get-datastore コマンドを使用して AWS CLI 、特定のデータストアで OIDC 認証機能が有効になっているかどうか、および属性lambdaAuthorizerArn」が存在するかどうかを確認できます。

```
aws medical-imaging get-datastore --datastore-id YourDatastoreId
```

```
{
    "datastoreProperties": {
        "datastoreId": YourdatastoreId,
        "datastoreName": YourDatastoreName,
        "datastoreStatus": "ACTIVE",
        "lambdaAuthorizerArn": YourAuthorizerFunctionArn,
        "datastoreArn": YourDatastoreArn,
        "createdAt": "2025-09-30T14:16:04.015000-05:00",
        "updatedAt": "2025-09-30T14:16:04.015000-05:00"
    }
}
```

**注記**  
 AWS CLI データストア作成コマンドの実行ロールには、Lambda オーソライザー関数を呼び出すための適切なアクセス許可が必要です。これにより、悪意のあるユーザーがデータストアオーソライザー設定を通じて不正な Lambda 関数を実行できる特権エスカレーション攻撃が軽減されます。

## 例外コード
<a name="dicomweb-oidc-exceptions"></a>

認証に失敗した場合、HealthImaging は次の HTTP エラーレスポンスコードと本文メッセージを返します。


| Condition | AHI レスポンス | 
| --- | --- | 
| Lambda オーソライザーが存在しないか、無効です | 424 オーソライザーの設定ミス | 
| 実行の失敗によりオーソライザーが終了しました | 424 オーソライザーが失敗しました | 
| マッピングされていない他のオーソライザーエラー | 424 オーソライザーが失敗しました | 
| オーソライザーが無効な/不正な形式のレスポンスを返しました | 424 オーソライザーの設定ミス | 
| オーソライザーが 1 秒以上実行されました | 408 オーソライザータイムアウト | 
| トークンの有効期限が切れているか、無効です | 403 無効または期限切れのトークン | 
| オーソライザーの設定ミスにより、AHI は返された IAM ロールをフェデレーションできません | 424 オーソライザーの設定ミス | 
| オーソライザーが空のロールを返しました | 403 アクセスが拒否されました | 
| 返されたロールは呼び出せません (assume-role/trust misconfig) | 424 オーソライザーの設定ミス | 
| リクエストレートが DICOMweb Gateway の制限を超えています | 429 リクエストが多すぎる | 
| データストア、リターンロール、またはオーソライザーのクロスアカウント/クロスリージョン | 424 オーソライザークロスアカウント/クロスリージョンアクセス | 

## 実装例
<a name="dicomweb-oidc-implementation"></a>

この Python の例は、HealthImaging イベントからの AWS Cognito アクセストークンを検証し、適切な DICOMWeb 権限を持つ IAM ロール ARN を返す Lambda オーソライザー関数を示しています。

Lambda オーソライザーは、2 つのキャッシュメカニズムを実装して、外部呼び出しと応答のレイテンシーを削減します。JWKS (JSON ウェブキーセット) は 1 時間に 1 回フェッチされ、関数の一時フォルダに保存されるため、後続の関数呼び出しはパブリックネットワークからフェッチする代わりにローカルで読み取ることができます。token\$1cache ディクショナリオブジェクトがこの Lambda 関数のグローバルコンテキストでインスタンス化されていることもわかります。グローバル変数は、同じウォームされた Lambda コンテキストを再利用するすべての呼び出しによって共有されます。これにより、正常に検証されたトークンをこのディクショナリに保存し、この同じ Lambda 関数の次の実行中にすばやく検索できます。キャッシュ方法は、ほとんどの ID プロバイダーから発行されたアクセストークンに適合する可能性がある一般論的なアプローチを表します。 AWS Cognito 固有のキャッシュオプションについては、[AWS Cognito ドキュメント](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html)の[「ユーザープールの管理](https://docs.aws.amazon.com/cognito/latest/developerguide/managing-users.html)」セクションと[「キャッシュ」セクション](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-caching-tokens.html)を参照してください。

```
import json
import os
import time
import logging
from jose import jwk, jwt
from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError
import requests
import tempfile

# Configure logging
logger = logging.getLogger()
log_level = os.environ.get('LOG_LEVEL', 'WARNING').upper()
logger.setLevel(getattr(logging, log_level, logging.WARNING))

# Global token cache with TTL
token_cache = {}

# JWKS cache file path
JWKS_CACHE_FILE = os.path.join(tempfile.gettempdir(), 'jwks.json')
JWKS_CACHE_TTL = 3600  # 1 hour

# Load environment variables once
USER_POOL_ID = os.environ['USER_POOL_ID']
CLIENT_ID = os.environ['CLIENT_ID']
ROLE_ARN = os.environ.get('AHIDICOMWEB_READONLY_ROLE_ARN', '')

def cleanup_expired_tokens():
    """Remove expired tokens from cache"""
    now = int(time.time())
    expired_keys = [token for token, data in token_cache.items() if now > data['cache_expiry']]
    for token in expired_keys:
        del token_cache[token]

def get_cached_jwks():
    """Get JWKS from cache file if valid, otherwise return None """
    try:
        if os.path.exists(JWKS_CACHE_FILE):
            # Check if cache file is still valid
            cache_age = time.time() - os.path.getmtime(JWKS_CACHE_FILE)
            if cache_age < JWKS_CACHE_TTL:
                with open(JWKS_CACHE_FILE, 'r') as f:
                    jwks = json.load(f)
                    logger.debug(f'Using cached JWKS (age: {int(cache_age)}s)')
                    return jwks
            else:
                logger.debug(f'JWKS cache expired (age: {int(cache_age)}s)')
    except Exception as e:
        logger.debug(f'Error reading JWKS cache: {e}')
    
    return None

def cache_jwks(jwks):
    """Cache JWKS to file"""
    try:
        with open(JWKS_CACHE_FILE, 'w') as f:
            json.dump(jwks, f)
        logger.debug('JWKS cached successfully')
    except Exception as e:
        logger.debug(f'Error caching JWKS: {e}')

def fetch_jwks(jwks_url):
    """Fetch JWKS from URL and cache it"""
    logger.debug('Fetching JWKS from URL')
    jwks = requests.get(jwks_url, timeout=10).json()
    # Convert to dict for faster lookups
    jwks['keys_by_kid'] = {key['kid']: key for key in jwks['keys']}
    cache_jwks(jwks)
    return jwks

def is_token_cached(token):
    if token not in token_cache:
        return None
    
    cached = token_cache[token]
    now = int(time.time())
    
    if now > cached['cache_expiry']:
        del token_cache[token]
        return None
    
    return cached

def cache_token(token, payload):
    now = int(time.time())
    token_exp = payload.get('exp')
    cache_expiry = min(now + 60, token_exp)  # 1 minute or token expiry, whichever is sooner
    
    token_cache[token] = {
        'payload': payload,
        'cache_expiry': cache_expiry,
        'role_arn': ROLE_ARN
    }

def handler(event, context):
    cleanup_expired_tokens() # start be removing expired tokens from the cache
    try:
        # Extract token from bearerToken or authorizationToken field
        token = event.get('bearerToken')
        if not token:
            raise Exception('No token provided')
        
        # Check cache first
        cached = is_token_cached(token)
        if cached:
            logger.debug('Token found in cache, skipping verification')
            return {
                'isTokenValid': True,
                'roleArn': cached['role_arn']
            }
        
        # Get Cognito configuration
        region = context.invoked_function_arn.split(':')[3]
        
        # Get JWKS (cached or fresh)
        jwks_url = f'https://cognito-idp.{region}.amazonaws.com/{USER_POOL_ID}/.well-known/jwks.json'
        jwks = get_cached_jwks()
        if not jwks:
            jwks = fetch_jwks(jwks_url)
        
        # Decode token header to get kid
        headers = jwt.get_unverified_headers(token)
        kid = headers['kid']
        
        # Find the correct key
        key = None
        for jwk_key in jwks['keys']:
            if jwk_key['kid'] == kid:
                key = jwk_key
                break
        
        if not key:
            # Key not found - try refreshing JWKS in case of key rotation
            logger.debug('Key not found in cached JWKS, fetching fresh JWKS')
            jwks = fetch_jwks(jwks_url)
            for jwk_key in jwks['keys']:
                if jwk_key['kid'] == kid:
                    key = jwk_key
                    break
        
        if not key:
            raise Exception('Public key not found')
        
        # Construct the public key
        public_key = jwk.construct(key)
        
        # Verify and decode the token (includes expiry validation)
        payload = jwt.decode(
            token,
            public_key,
            algorithms=['RS256'],
            audience=CLIENT_ID,
            issuer=f'https://cognito-idp.{region}.amazonaws.com/{USER_POOL_ID}'
        )
        
        logger.debug('Token validated successfully')
        logger.debug('User: %s', payload.get('username', 'unknown'))
        
        # Cache the validated token
        cache_token(token, payload)
        
        # Return authorization response
        return {
            'isTokenValid': True,
            'roleArn': ROLE_ARN
        }
        
    except ExpiredSignatureError:
        logger.debug('Token expired')
        return {
            'isTokenValid': False,
            'roleArn': ''
        }
    except JWTClaimsError:
        logger.debug('Invalid token claims')
        return {
            'isTokenValid': False,
            'roleArn': ''
        }
    except JWTError as e:
        logger.debug('JWT validation error: %s', e)
        return {
            'isTokenValid': False,
            'roleArn': ''
        }
    except Exception as e:
        logger.debug('Authorization failed: %s', e)
        return {
            'isTokenValid': False,
            'roleArn': ''
        }
```