

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# DICOMweb APIs 대한 OIDC 인증
<a name="dicomweb-oidc"></a>

AWS HealthImaging은 기존 서명 버전 [4(SigV4) 인증 외에도 OpenID Connect(OIDC)를 사용한 DICOMweb API 요청에 대해 OAuth 2.0](https://oauth.net/2/) 기반 인증을 지원합니다. DICOMweb [OpenID ](https://openid.net/specs/openid-connect-core-1_0.html) [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 토큰(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. **자격 증명 공급자로 확인:** Lambda에는 ID 토큰 서명을 확인하고, 더 광범위한 토큰 확인(예: 발급자, 대상, 사용자 지정 클레임)을 수행하고, 필요한 경우 IdP에 대해 해당 클레임을 검증하는 사용자 지정 코드가 포함되어 있습니다.

1. **권한 부여자가 액세스 정책을 반환합니다.** 확인에 성공하면 Lambda 함수가 인증된 사용에 적합한 권한을 결정합니다. 그런 다음 Lambda 권한 부여자는 부여할 권한 집합을 나타내는 IAM 역할의 Amazon 리소스 이름(ARN)을 반환합니다.

1. **요청 실행:** 수임된 IAM 역할에 필요한 권한이 있는 경우 HealthImaging은 요청된 DICOMWeb 리소스를 반환합니다. 권한이 충분하지 않으면 HealthImaging은 요청을 거부하고 적절한 오류 응답 오류(예: 403 금지됨)를 반환합니다.

**참고**  
AWS HealthImaging 서비스에서 관리하지 않는 권한 부여자 Lambda 함수입니다. AWS 계정에서 실행됩니다. 고객에게는 함수 호출 및 실행 시간에 대한 요금이 HealthImaging 요금과 별도로 청구됩니다.

## 아키텍처 개요
<a name="dicomweb-oidc-architecture-overview"></a>

![\[워크플로를 보여주는 다이어그램: 클라이언트에서 토큰 전송, Lambda 권한 부여자 검증, HealthImaging 프로세스 요청\]](http://docs.aws.amazon.com/ko_kr/healthimaging/latest/devguide/images/security-oidc-workflow-lambda.png)


## 사전 조건
<a name="dicomweb-oidc-prerequisites"></a>

### 액세스 토큰 요구 사항
<a name="dicomweb-oidc-token-requirements"></a>

HealthImaging에서는 액세스 토큰이 JSON 웹 토큰(JWT) 형식이어야 합니다. 많은 ID 제공업체(IDPs)는 기본적으로이 토큰 형식을 제공하는 반면 액세스 토큰 양식을 선택하거나 구성할 수 있는 ID 제공업체도 있습니다. 통합을 진행하기 전에 선택한 IDP가 JWT 토큰을 발급할 수 있는지 확인합니다.

토큰 형식  
액세스 토큰은 JWT(JSON 웹 토큰) 형식이어야 합니다.

필수 클레임    
`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 웹 키 세트) 캐시
+ 가능한 경우 유효한 액세스 토큰 캐시
+ 자격 증명 공급자에 대한 네트워크 호출 최소화
+ 효율적인 토큰 검증 로직 구현

### Lambda 구성
<a name="dicomweb-oidc-lambda-configuration"></a>
+ Python 및 Node.js 기반 함수는 일반적으로 더 빠르게 초기화됩니다.
+ 로드할 외부 라이브러리의 양 줄이기
+ 일관된 성능을 보장하기 위해 적절한 메모리 할당 구성
+ CloudWatch 지표를 사용하여 실행 시간 모니터링

## OIDC 인증 활성화
<a name="dicomweb-oidc-enablement"></a>
+ OIDC 인증은 **새** 데이터 스토어를 생성할 때**만** 활성화할 수 있습니다.
+ 기존 데이터 스토어에 대한 OIDC 활성화는 API를 통해 지원되지 않습니다.
+ 기존 데이터 스토어에서 OIDC를 활성화하려면 고객이 AWS Support에 문의해야 합니다.

# OIDC 인증을 위한 AWS Lambda 권한 부여자 설정
<a name="dicomweb-oidc-requirements"></a>

이 안내서에서는 HealthImaging OIDC 인증 기능의 요구 사항과 호환되는 액세스 토큰을 제공하도록 선택한 ID 제공업체(IdP)를 이미 구성했다고 가정합니다.

## 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
```

이 리소스 정책은 DICOMWeb API 요청을 인증할 때 HealthImaging 서비스가 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 함께를 사용하여 새 데이터 스토어를 생성해야 합니다. Support에 문의 AWS 하지 않으면 기존 데이터 스토어에서 OIDC 인증을 활성화할 수 없습니다.

다음은 OIDC 인증이 활성화된 새 데이터 스토어를 생성하는 방법의 예입니다.

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

get-datastore 명령을 사용하고 AWS CLI "lambdaAuthorizerArn" 속성이 있는지 확인하여 특정 데이터 스토어에 OIDC 인증 기능이 활성화되어 있는지 확인할 수 있습니다.

```
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 오류 응답 코드와 본문 메시지를 반환합니다.


| 조건 | 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 권한 부여자는 외부 호출과 응답 지연 시간을 줄이기 위해 두 가지 캐싱 메커니즘을 구현합니다. JWKS(JSON 웹 키 세트)는 1시간마다 한 번씩 가져와 함수의 임시 폴더에 저장되므로 후속 함수 호출은 퍼블릭 네트워크에서 가져오는 대신 로컬에서 읽을 수 있습니다. 또한이 Lambda 함수의 전역 컨텍스트에서 token\$1cache 딕셔너리 객체가 인스턴스화됩니다. 전역 변수는 동일한 웜 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': ''
        }
```