Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BFF_AUTHENTICATION_DOCS_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ All authentication configuration now uses `AUTH_*` parameters. **Deprecated `OID
- **AUTH_OIDC_VERIFY_SSL**: Whether to verify SSL certificates for OIDC requests (default: `true`)
- **AUTH_OIDC_VERIFY_SIGNATURE**: Whether to verify OIDC token signatures (default: `true`)
- **AUTH_SESSION_TTL_HOURS**: Session duration in hours (default: `24`)
- **AUTH_KEY_VERSIONS_TO_KEEP**: Number of encryption key versions to retain during rotation (default: `3`)
- **AUTH_SYNC_DOMAINS**: Optional comma-separated list of additional domains for cookie sync
- **AUTH_SESSION_TABLE_NAME**: DynamoDB table name for authentication sessions (default: `mlspace-auth-sessions`)
- **AUTH_TOKEN_ENCRYPTION_KEY_SECRET_NAME**: Secrets Manager secret name for token encryption keys (default: `mlspace/auth/token-encryption-keys`)
Expand Down Expand Up @@ -130,6 +131,7 @@ frontend/docs/admin-guide/
- **AUTH_OIDC_VERIFY_SSL**: Whether to verify SSL certificates (default: `true`)
- **AUTH_OIDC_VERIFY_SIGNATURE**: Whether to verify OIDC token signatures (default: `true`)
- **AUTH_SESSION_TTL_HOURS**: Session duration configuration (default: `24`)
- **AUTH_KEY_VERSIONS_TO_KEEP**: Number of encryption key versions to retain during rotation (default: `3`)
- **AUTH_SYNC_DOMAINS**: Multi-domain cookie synchronization
- **AUTH_SESSION_TABLE_NAME**: DynamoDB table name for sessions
- **AUTH_TOKEN_ENCRYPTION_KEY_SECRET_NAME**: Versioned token encryption keys (rotatable)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ If the config-helper doesn't provide the level of customization you need for you
| AUTH_OIDC_VERIFY_SIGNATURE | Whether or not the lambda authorizer should verify the JWT token signature | `true` |
| AUTH_OIDC_USE_PKCE | Whether to use PKCE (Proof Key for Code Exchange) for OIDC authentication | `false` |
| AUTH_SESSION_TTL_HOURS | The time-to-live (TTL) in hours for authentication sessions | `24` |
| AUTH_KEY_VERSIONS_TO_KEEP | Number of encryption key versions to retain during key rotation | `3` |
| AUTH_SYNC_DOMAINS | Comma-separated list of domains to sync authentication state across | - |
| ADDITIONAL_LAMBDA_ENVIRONMENT_VARS | A map of key value pairs which will be set as environment variables on every MLSpace lambda | `{}` |
| RESOURCE_TERMINATION_INTERVAL | Interval (in minutes) to run the resource termination cleanup lambda | `60` |
Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ moto==4.0.8
dynamodb-json==1.3
cachetools==5.3.2
pyseto==1.7.8
authlib==1.2.1
authlib==1.6.6
pydantic==2.5.3
requests==2.32.5
cryptography==42.0.8
19 changes: 4 additions & 15 deletions backend/src/ml_space_lambda/auth/handlers/oidc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,14 @@ def _create_oauth_session(self):
"""
Create OAuth2 session for token operations using authlib.
"""
# Create a requests session with SSL verification setting
session = requests.Session()
session.verify = self.config.verify_ssl

self.oauth_session = OAuth2Session(
client_id=self.config.client_id,
client_secret=self.config.client_secret,
scope=" ".join(self.config.scopes),
token_endpoint=self.token_endpoint,
token_endpoint_auth_method="client_secret_post" if self.config.client_secret else None,
verify=self.config.verify_ssl,
)
# Set the session on the OAuth2Session to use our configured session
self.oauth_session.session = session

def get_authorization_url(self, state: str, redirect_uri: str, code_verifier: Optional[str] = None) -> str:
"""
Expand Down Expand Up @@ -178,7 +173,7 @@ def get_authorization_url(self, state: str, redirect_uri: str, code_verifier: Op
logger.info("Generated OIDC authorization URL")

# Create temporary OAuth2 session for URL generation
client = OAuth2Session(**session_params)
client = OAuth2Session(verify=self.config.verify_ssl, **session_params)
authorization_url, _ = client.create_authorization_url(self.authorization_endpoint, **authorization_params)

return authorization_url
Expand Down Expand Up @@ -283,10 +278,7 @@ def get_user_info(self, access_token: str) -> UserData:
oauth_token = OAuth2Token({"access_token": access_token, "token_type": "Bearer"})

# Create a temporary session with the token
session = OAuth2Session(client_id=self.config.client_id, token=oauth_token)
# Apply SSL verification setting
session.session = requests.Session()
session.session.verify = self.config.verify_ssl
session = OAuth2Session(client_id=self.config.client_id, token=oauth_token, verify=self.config.verify_ssl)

# Get user info using authlib
resp = session.get(self.userinfo_endpoint)
Expand Down Expand Up @@ -526,10 +518,7 @@ def _get_user_info_from_oauth_token(self, oauth_token: OAuth2Token) -> UserData:
if access_token and self.userinfo_endpoint:
try:
# Use authlib's OAuth2Session to get user info
session = OAuth2Session(client_id=self.config.client_id, token=oauth_token)
# Apply SSL verification setting
session.session = requests.Session()
session.session.verify = self.config.verify_ssl
session = OAuth2Session(client_id=self.config.client_id, token=oauth_token, verify=self.config.verify_ssl)

resp = session.get(self.userinfo_endpoint)
if resp.status_code == 200:
Expand Down
30 changes: 25 additions & 5 deletions backend/src/ml_space_lambda/auth/utils/key_rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import json
import logging
import os
from typing import Dict, Optional

import boto3
Expand All @@ -41,6 +42,20 @@
logger.setLevel(level=logging.INFO)


def _get_default_keep_versions() -> int:
"""
Get the default number of key versions to keep from environment variable.

Returns:
Number of versions to keep (default: 3)
"""
try:
return int(os.environ.get("AUTH_KEY_VERSIONS_TO_KEEP", "3"))
except ValueError:
logger.warning("Invalid AUTH_KEY_VERSIONS_TO_KEEP value, using default: 3")
return 3


def initialize_state_encryption_key(secret_arn: str) -> Dict:
"""
Initialize state encryption key secret with versioned structure.
Expand Down Expand Up @@ -129,7 +144,7 @@ def rotate_state_encryption_key(
secret_arn: str,
version_stage: str = SecretsManagerStage.PENDING,
version_token: Optional[str] = None,
keep_versions: int = 3,
keep_versions: Optional[int] = None,
) -> KeyRotationResult:
"""
Rotate state encryption key for AWS Secrets Manager rotation protocol.
Expand All @@ -139,15 +154,18 @@ def rotate_state_encryption_key(

Args:
secret_arn: AWS Secrets Manager ARN for state encryption key
token: Version stage (AWSPENDING for new version)
keep_versions: Number of recent versions to keep after rotation
version_stage: Version stage (AWSPENDING for new version)
version_token: Version token for the rotation
keep_versions: Number of recent versions to keep after rotation (uses AUTH_KEY_VERSIONS_TO_KEEP env var if not specified)

Returns:
KeyRotationResult with rotation details

Raises:
Exception: If rotation fails
"""
if keep_versions is None:
keep_versions = _get_default_keep_versions()
try:
secrets_client = boto3.client("secretsmanager")

Expand Down Expand Up @@ -205,7 +223,7 @@ def rotate_token_encryption_key(
secret_arn: str,
version_stage: str = SecretsManagerStage.PENDING,
version_token: Optional[str] = None,
keep_versions: int = 3,
keep_versions: Optional[int] = None,
) -> KeyRotationResult:
"""
Rotate token encryption key for AWS Secrets Manager rotation protocol.
Expand All @@ -217,14 +235,16 @@ def rotate_token_encryption_key(
secret_arn: AWS Secrets Manager ARN for token encryption key
version_stage: Version stage (AWSPENDING for new version)
version_token: Version token (normally from ClientRequestToken)
keep_versions: Number of recent versions to keep after rotation
keep_versions: Number of recent versions to keep after rotation (uses AUTH_KEY_VERSIONS_TO_KEEP env var if not specified)

Returns:
KeyRotationResult with rotation details

Raises:
Exception: If rotation fails
"""
if keep_versions is None:
keep_versions = _get_default_keep_versions()
try:
secrets_client = boto3.client("secretsmanager")

Expand Down
22 changes: 20 additions & 2 deletions backend/src/ml_space_lambda/auth/utils/rotation_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"""

import logging
import os
from enum import StrEnum
from typing import Any, Dict, Optional

Expand All @@ -39,6 +40,20 @@
logger.setLevel(level=logging.INFO)


def _get_keep_versions() -> int:
"""
Get the number of key versions to keep from environment variable.

Returns:
Number of versions to keep (default: 3)
"""
try:
return int(os.environ.get("AUTH_KEY_VERSIONS_TO_KEEP", "3"))
except ValueError:
logger.warning("Invalid AUTH_KEY_VERSIONS_TO_KEEP value, using default: 3")
return 3


class RotationStep(StrEnum):
"""AWS Secrets Manager rotation steps."""

Expand Down Expand Up @@ -86,12 +101,15 @@ def _handle_create_secret_step(secret_name: str, key_type: str, version_token: O
Args:
secret_name: Secret ARN
key_type: Type of key being rotated
version_token: Version token for the rotation
"""
keep_versions = _get_keep_versions()

# Create new key version
if key_type == KeyType.STATE:
result = rotate_state_encryption_key(secret_name, version_token=version_token, keep_versions=3)
result = rotate_state_encryption_key(secret_name, version_token=version_token, keep_versions=keep_versions)
else: # token
result = rotate_token_encryption_key(secret_name, version_token=version_token, keep_versions=3)
result = rotate_token_encryption_key(secret_name, version_token=version_token, keep_versions=keep_versions)


def _handle_set_secret_step(secret_name: str) -> None:
Expand Down
46 changes: 45 additions & 1 deletion bin/mlspace-cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
limitations under the License.
*/

import { App, Aspects, Tags } from 'aws-cdk-lib';
import { App, Aspects, Tags, DefaultStackSynthesizer, CliCredentialsStackSynthesizer, LegacyStackSynthesizer } from 'aws-cdk-lib';
import { LogGroup } from 'aws-cdk-lib/aws-logs';
import 'source-map-support/register';
import { AdminApiStack } from '../lib/stacks/api/admin';
Expand Down Expand Up @@ -51,6 +51,37 @@ const envProperties = {
};

const app = new App();

// Read synthesizer configuration from cdk.json
const synthesizerConfig = app.node.tryGetContext('synthesizer') || {};
const synthesizerType = synthesizerConfig.type || 'default';

// Create synthesizer based on configuration
function createSynthesizer () {
// Extract type and pass remaining config as parameters
const { ...synthesizerParams } = synthesizerConfig;

// Convert empty strings to undefined for optional parameters
const cleanParams = Object.fromEntries(
Object.entries(synthesizerParams).map(([key, value]) => [
key,
value === '' ? undefined : value
])
);

switch (synthesizerType) {
case 'default':
return new DefaultStackSynthesizer(cleanParams);
case 'cli':
return new CliCredentialsStackSynthesizer(cleanParams);
case 'legacy':
return new LegacyStackSynthesizer();
default:
return undefined; // Use CDK default
}
}

const synthesizer = createSynthesizer();
const stacks = [];
const isIso = ['us-iso-east-1', 'us-isob-east-1'].includes(config.AWS_REGION);

Expand All @@ -59,6 +90,7 @@ const enableTranslate = !['us-isob-east-1'].includes(config.AWS_REGION);

const vpcStack = new VPCStack(app, 'mlspace-vpc', {
env: envProperties,
synthesizer,
deployCFNEndpoint: true,
deployCWEndpoint: true,
deployCWLEndpoint: true,
Expand All @@ -74,6 +106,7 @@ const mlSpaceVPC = vpcStack.vpc;

const kmsStack = new KMSStack(app, 'mlspace-kms', {
env: envProperties,
synthesizer,
keyManagerRoleName: config.KEY_MANAGER_ROLE_NAME,
mlspaceConfig: config
});
Expand All @@ -87,6 +120,7 @@ const accessLogsBucketName = `${config.ACCESS_LOGS_BUCKET_NAME}-${config.AWS_ACC

const iamStack = new IAMStack(app, 'mlspace-iam', {
env: envProperties,
synthesizer,
dataBucketName,
configBucketName,
websiteBucketName,
Expand Down Expand Up @@ -114,6 +148,7 @@ const frontEndAssetsPath = './frontend/build/';

const coreStack = new CoreStack(app, 'mlspace-core', {
env: envProperties,
synthesizer,
dataBucketName,
configBucketName,
websiteBucketName,
Expand All @@ -139,12 +174,14 @@ stacks.push(coreStack);

stacks.push(new SagemakerStack(app, 'mlspace-sagemaker', {
env: envProperties,
synthesizer,
dataBucketName,
mlspaceConfig: config
}));

const restStack = new RestApiStack(app, 'mlspace-web-tier', {
env: envProperties,
synthesizer,
dataBucketName,
websiteBucketName,
websiteS3ReaderRole,
Expand All @@ -166,6 +203,7 @@ stacks.push(restStack);

const apiStackProperties: ApiStackProperties = {
env: envProperties,
synthesizer,
restApiId: restStack.mlSpaceRestApiId,
rootResourceId: restStack.mlSpaceRestApiRootResourceId,
dataBucketName,
Expand Down Expand Up @@ -208,9 +246,15 @@ if (enableTranslate) {
}
const apiDeploymentStack = new ApiDeploymentStack(app, 'mlspace-api-deployment', {
env: envProperties,
synthesizer,
restApiId: restStack.mlSpaceRestApiId,
});

// add ADCLambdaCABundleAspect to core in ADC regions
if (isIso) {
Aspects.of(coreStack).add(new ADCLambdaCABundleAspect());
}

apiStacks.forEach((stack) => {
stack.addDependency(coreStack);
stack.addDependency(iamStack);
Expand Down
10 changes: 6 additions & 4 deletions cypress/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cypress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"main": "index.ts",
"version": "1.0.0",
"dependencies": {
"cypress-file-upload": "^5.0.8"
"cypress-file-upload": "^5.0.8",
"lodash": "^4.17.23"
},
"devDependencies": {
"@cloudscape-design/components": "^3.0.886",
Expand Down
Loading
Loading