From 104e7d8089a61245b5d2b4addb9a953b61d2db97 Mon Sep 17 00:00:00 2001 From: DeepaliTandale <18361446+DeepaliTandale@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:38:58 +0100 Subject: [PATCH 1/3] Inital commit for Lambda-pingidentity-python --- lambda-pingidentity-python | 1 + 1 file changed, 1 insertion(+) create mode 160000 lambda-pingidentity-python diff --git a/lambda-pingidentity-python b/lambda-pingidentity-python new file mode 160000 index 000000000..37189d6ca --- /dev/null +++ b/lambda-pingidentity-python @@ -0,0 +1 @@ +Subproject commit 37189d6ca90fd9657237c6f303c6e31b1afc46db From 07b0e479e29dafc494a204bf2d8fcc4a180287be Mon Sep 17 00:00:00 2001 From: DeepaliTandale <18361446+DeepaliTandale@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:35:02 +0100 Subject: [PATCH 2/3] deleted folder --- lambda-pingidentity-python | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lambda-pingidentity-python diff --git a/lambda-pingidentity-python b/lambda-pingidentity-python deleted file mode 160000 index 37189d6ca..000000000 --- a/lambda-pingidentity-python +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 37189d6ca90fd9657237c6f303c6e31b1afc46db From aff65fe6b6f3a68f564d5eeff0fb533fa802e7d0 Mon Sep 17 00:00:00 2001 From: DeepaliTandale <18361446+DeepaliTandale@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:38:57 +0100 Subject: [PATCH 3/3] initial commit --- .../ArchitectureDiagram.jpg | Bin 0 -> 39526 bytes lambda-pingidentity-python/LICENSE | 21 +++ lambda-pingidentity-python/README.md | 160 ++++++++++++++++++ lambda-pingidentity-python/get_token.sh | 88 ++++++++++ .../src/jwt_authorizer.py | 107 ++++++++++++ .../src/requirements.txt | 3 + lambda-pingidentity-python/src/user_info.py | 48 ++++++ lambda-pingidentity-python/template.yaml | 66 ++++++++ 8 files changed, 493 insertions(+) create mode 100644 lambda-pingidentity-python/ArchitectureDiagram.jpg create mode 100644 lambda-pingidentity-python/LICENSE create mode 100644 lambda-pingidentity-python/README.md create mode 100755 lambda-pingidentity-python/get_token.sh create mode 100644 lambda-pingidentity-python/src/jwt_authorizer.py create mode 100644 lambda-pingidentity-python/src/requirements.txt create mode 100644 lambda-pingidentity-python/src/user_info.py create mode 100644 lambda-pingidentity-python/template.yaml diff --git a/lambda-pingidentity-python/ArchitectureDiagram.jpg b/lambda-pingidentity-python/ArchitectureDiagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5195418b55817a782ce5d2ffe0c2b42826cbf7ab GIT binary patch literal 39526 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�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 literal 0 HcmV?d00001 diff --git a/lambda-pingidentity-python/LICENSE b/lambda-pingidentity-python/LICENSE new file mode 100644 index 000000000..96aa4d110 --- /dev/null +++ b/lambda-pingidentity-python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Deepali Tandale + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lambda-pingidentity-python/README.md b/lambda-pingidentity-python/README.md new file mode 100644 index 000000000..d61961bc0 --- /dev/null +++ b/lambda-pingidentity-python/README.md @@ -0,0 +1,160 @@ +# Simple JWT API with PingOne Integration + +A minimal serverless API demonstrating JWT authentication with PingOne using AWS Lambda and API Gateway. + +## Architecture + +### High-Level Flow +![project2025-Severless-Page-2.jpg](ArchitectureDiagram.jpg) + +## Technologies Used + +- **AWS Lambda** - Serverless compute +- **API Gateway** - HTTP API with custom authorizer +- **PingOne** - Identity provider and JWT issuer +- **Python 3.9** - Runtime environment +- **AWS SAM** - Infrastructure as Code + +### Detailed Authentication Flow + +When a client sends an HTTP request to a protected API, it includes a JWT token in the `Authorization: Bearer ` header. This request is received by the API Gateway, which is configured with a custom JWT authorizer. The authorizer function is triggered and extracts the JWT from the request. It then fetches the public keys from PingOne’s JWKS (JSON Web Key Set) endpoint to validate the token. During this process, the authorizer verifies the JWT’s signature, ensures it was issued by a trusted PingOne environment, and checks that the token has not expired. If everything is valid, the authorizer returns an IAM policy that either allows or denies access, along with any relevant user context. If access is granted, the request proceeds to the protected Lambda function behind the API. This Lambda uses the user context provided by the authorizer to generate a response. Finally, the API Gateway sends this response back to the client, including any necessary CORS headers to support web-based applications. + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- AWS SAM CLI installed +- Python 3.9+ installed +- A PingOne account and environment + +## Setup and Deployment + +### Step 1: PingOne Configuration + +1. **Log into PingOne Admin Console** + - Go to your PingOne admin portal + - Navigate to your environment + +2. **Create OIDC Application** + - Go to Applications → Add Application + - Choose "OIDC Web App" + - Configure the following: + - **Name**: Simple JWT API + - **Redirect URIs**: `http://localhost:3000/callback` + - **Grant Types**: Authorization Code + - **Response Types**: Code + - **Scopes**: openid, profile, email + +3. **Note Your Configuration** + - **Environment ID**: Found in the URL or environment settings + - **Client ID**: From your application settings + - **Client Secret**: From your application settings + + +### Step 2: Build & Deploy + +1. **Build the Application** + ```bash + sam build + ``` + +2. **Deploy the Application** + ```bash + sam deploy --guided + ``` + +3. **Note the API Endpoint** + The deployment will output your API Gateway endpoint URL. + +### Step 3: Generate Access Token + +You can generate PingOne JWT tokens using multiple methods: + +
+
Click to see various token generation methods + +#### Option 1: Using the provided script +```bash +# Set environment variables +export CLIENT_ID=your-client-id +export CLIENT_SECRET=your-client-secret +export ENVIRONMENT_ID=your-environment-id + +# Get authorization URL +./get_token.sh + +# Exchange auth code for token +./get_token.sh YOUR_AUTH_CODE +``` + +#### Option 2: PingOne Admin Console +- Log into your PingOne admin console +- Navigate to Applications → Your App → Configuration +- Use the "Test Connection" or token generation features + +#### Option 3: Direct OAuth2 Flow +1. **Authorization URL:** + ``` + https://auth.pingone.com/YOUR_ENV_ID/as/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost:3000/callback&scope=openid%20profile%20email + ``` + +2. **Token Exchange (curl):** + ```bash + curl -X POST "https://auth.pingone.com/YOUR_ENV_ID/as/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \ + -d "grant_type=authorization_code&code=YOUR_AUTH_CODE&redirect_uri=http://localhost:3000/callback" + ``` + +#### Option 4: Postman/Insomnia +- Import PingOne OAuth2 collection +- Configure your environment variables +- Use the authorization code flow + + + +### Step 4: Test the API + +Once you have an access token from any method above: + +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" YOUR_API_ENDPOINT +``` + +## API Endpoints + +### GET /user +Returns user information after successful JWT verification. + +**Response:** +```json +{ + "success": true, + "message": "JWT verification successful - Connection established", + "user": "", + "scope": "openid profile email", + "timestamp": 29999 +} +``` + + +## Project Structure + +``` +├── src/ +│ ├── jwt_authorizer.py # Custom JWT authorizer function +│ ├── user_info.py # Protected API endpoint +│ └── requirements.txt # Python dependencies +├── template.yaml # SAM template +├── get_token.sh # Token generation helper (optional) +└── samconfig.toml.example # SAM configuration template +``` + +## Troubleshooting + +- **Invalid Token**: Check that your PingOne configuration matches your deployment +- **CORS Issues**: Ensure your redirect URI is properly configured in PingOne +- **Deployment Errors**: Verify your AWS credentials and permissions +- **Token Expiration**: PingOne tokens have limited lifespans, generate new ones as needed +- **Script Issues**: If `get_token.sh` doesn't work, use alternative token generation methods listed above + + diff --git a/lambda-pingidentity-python/get_token.sh b/lambda-pingidentity-python/get_token.sh new file mode 100755 index 000000000..d2a5f2f05 --- /dev/null +++ b/lambda-pingidentity-python/get_token.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +#!/bin/bash + +# PingOne Configuration from environment variables +CLIENT_ID="${CLIENT_ID}" +CLIENT_SECRET="${CLIENT_SECRET}" +ENVIRONMENT_ID="${ENVIRONMENT_ID}" +REDIRECT_URI="${REDIRECT_URI:-http://localhost:3000/callback}" + +# Validate required environment variables +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ] || [ -z "$ENVIRONMENT_ID" ]; then + echo "❌ Error: Missing required environment variables!" + echo "Please set the following environment variables:" + echo "- CLIENT_ID" + echo "- CLIENT_SECRET" + echo "- ENVIRONMENT_ID" + echo "" + echo "Example:" + echo "export CLIENT_ID=your-client-id" + echo "export CLIENT_SECRET=your-client-secret" + echo "export ENVIRONMENT_ID=your-environment-id" + echo "export REDIRECT_URI=http://localhost:3000/callback # optional" + exit 1 +fi + +# Create base64 encoded credentials +CREDENTIALS=$(echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64) + +echo "🔐 PingOne Token via Curl" +echo "=========================" +echo "" +echo "Your Configuration:" +echo "- Environment ID: ${ENVIRONMENT_ID}" +echo "- Client ID: ${CLIENT_ID}" +echo "- Redirect URI: ${REDIRECT_URI}" +echo "" + +echo "Step 1: Get Authorization Code" +echo "=============================" +echo "Open this URL in your browser:" +echo "" +echo "https://auth.pingone.com/${ENVIRONMENT_ID}/as/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&scope=openid%20profile%20email" +echo "" + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "" + echo "After getting the code from the browser, run:" + echo "$0 YOUR_AUTH_CODE" + exit 1 +fi + +AUTH_CODE=$1 + +echo "Step 2: Exchange Code for Token" +echo "===============================" +echo "Using authorization code: ${AUTH_CODE:0:20}..." +echo "" + +# Exchange code for token +RESPONSE=$(curl --silent --location --request POST "https://auth.pingone.com/${ENVIRONMENT_ID}/as/token" \ +--header 'Content-Type: application/x-www-form-urlencoded' \ +--header "Authorization: Basic ${CREDENTIALS}" \ +--data-urlencode 'grant_type=authorization_code' \ +--data-urlencode "code=${AUTH_CODE}" \ +--data-urlencode "redirect_uri=${REDIRECT_URI}") + +echo "Response:" +echo "${RESPONSE}" | python3 -m json.tool 2>/dev/null || echo "${RESPONSE}" +echo "" + +# Extract access token +ACCESS_TOKEN=$(echo "${RESPONSE}" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('access_token', 'NOT_FOUND'))" 2>/dev/null) + +if [ "${ACCESS_TOKEN}" != "NOT_FOUND" ] && [ "${ACCESS_TOKEN}" != "" ]; then + echo "Success! Access Token Retrieved" + echo "=================================" + echo "" + echo "Access Token:" + echo "${ACCESS_TOKEN}" + echo "" + echo "🧪 Test your API:" + echo "curl -H 'Authorization: Bearer ${ACCESS_TOKEN}' https://.execute-api..amazonaws.com/prod/user" +else + echo "Failed to get access token" + echo "Check the authorization code and try again" +fi diff --git a/lambda-pingidentity-python/src/jwt_authorizer.py b/lambda-pingidentity-python/src/jwt_authorizer.py new file mode 100644 index 000000000..f217ae7c8 --- /dev/null +++ b/lambda-pingidentity-python/src/jwt_authorizer.py @@ -0,0 +1,107 @@ +import json +import jwt +import os +from typing import Dict, Any +from jwt import PyJWKClient +from jwt.exceptions import InvalidTokenError, ExpiredSignatureError + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """ + PingOne JWT Authorizer Lambda function + Validates JWT tokens from PingOne and returns authorization policy + """ + print(f"PingOne Authorizer event: {json.dumps(event)}") + + try: + # Extract token from Authorization header + token = extract_token(event) + if not token: + print("No token found") + return generate_policy('user', 'Deny', event['methodArn']) + + # Validate JWT token with PingOne + payload = validate_pingone_jwt(token) + if not payload: + print("Invalid PingOne token") + return generate_policy('user', 'Deny', event['methodArn']) + + user_id = payload.get('sub', 'unknown') + print(f"PingOne token validated for user: {user_id}") + + # Generate allow policy with user context + policy = generate_policy(user_id, 'Allow', event['methodArn']) + policy['context'] = { + 'userId': user_id, + 'email': payload.get('email', ''), + 'scope': payload.get('scope', ''), + 'clientId': payload.get('client_id', ''), + 'issuer': payload.get('iss', ''), + 'tokenPayload': json.dumps(payload, default=str) + } + + return policy + + except Exception as e: + print(f"PingOne Authorizer error: {str(e)}") + return generate_policy('user', 'Deny', event['methodArn']) + +def extract_token(event: Dict[str, Any]) -> str: + """Extract JWT token from Authorization header""" + auth_header = event.get('authorizationToken', '') + + if auth_header.startswith('Bearer '): + return auth_header[7:] # Remove 'Bearer ' prefix + + return auth_header + +def validate_pingone_jwt(token: str) -> Dict[str, Any]: + """Validate JWT token with PingOne JWKS""" + try: + ping_issuer = os.environ.get('PING_ISSUER_URL') + ping_jwks_url = os.environ.get('PING_JWKS_URL') + + if not ping_issuer or not ping_jwks_url: + print("PingOne configuration missing") + return None + + # Get signing key from PingOne JWKS + jwks_client = PyJWKClient(ping_jwks_url) + signing_key = jwks_client.get_signing_key_from_jwt(token) + + # Validate token + payload = jwt.decode( + token, + signing_key.key, + algorithms=["RS256"], + issuer=ping_issuer, + options={"verify_aud": False} # Skip audience validation for simplicity + ) + + print(f"PingOne JWT payload: {json.dumps(payload, default=str)}") + return payload + + except ExpiredSignatureError: + print("PingOne token has expired") + return None + except InvalidTokenError as e: + print(f"Invalid PingOne token: {str(e)}") + return None + except Exception as e: + print(f"PingOne JWT validation error: {str(e)}") + return None + +def generate_policy(principal_id: str, effect: str, resource: str) -> Dict[str, Any]: + """Generate IAM policy for API Gateway""" + return { + 'principalId': principal_id, + 'policyDocument': { + 'Version': '2012-10-17', + 'Statement': [ + { + 'Action': 'execute-api:Invoke', + 'Effect': effect, + 'Resource': resource + } + ] + } + } diff --git a/lambda-pingidentity-python/src/requirements.txt b/lambda-pingidentity-python/src/requirements.txt new file mode 100644 index 000000000..ae3039d66 --- /dev/null +++ b/lambda-pingidentity-python/src/requirements.txt @@ -0,0 +1,3 @@ +PyJWT==2.8.0 +cryptography==41.0.7 +requests==2.31.0 diff --git a/lambda-pingidentity-python/src/user_info.py b/lambda-pingidentity-python/src/user_info.py new file mode 100644 index 000000000..0bc1fd2be --- /dev/null +++ b/lambda-pingidentity-python/src/user_info.py @@ -0,0 +1,48 @@ +import json +from typing import Dict, Any + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + try: + # Extract user context from authorizer (JWT already verified) + request_context = event.get('requestContext', {}) + authorizer = request_context.get('authorizer', {}) + + # Get basic user info from JWT + user_id = authorizer.get('userId', 'unknown') + scope = authorizer.get('scope', '') + + # Simple success response + response = { + 'statusCode': 200, + 'headers': { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + 'body': json.dumps({ + 'success': True, + 'message': 'JWT verification successful - Connection established', + 'user': user_id, + 'scope': scope, + 'timestamp': context.get_remaining_time_in_millis() + }) + } + + # Print successful connection + print(f" JWT verification successful - User {user_id} connected successfully") + return response + + except Exception as e: + print(f" Error: {str(e)}") + + return { + 'statusCode': 500, + 'headers': { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + 'body': json.dumps({ + 'success': False, + 'message': 'JWT verification failed - Connection error', + 'error': str(e) + }) + } diff --git a/lambda-pingidentity-python/template.yaml b/lambda-pingidentity-python/template.yaml new file mode 100644 index 000000000..f097cbe85 --- /dev/null +++ b/lambda-pingidentity-python/template.yaml @@ -0,0 +1,66 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Simple JWT API with custom authorizer + +Parameters: + PingOneIssuer: + Type: String + Description: PingOne Issuer URL + Default: "https://auth.pingone.com//as" + + PingOneJwksUrl: + Type: String + Description: PingOne JWKS URL for token validation + Default: "https://auth.pingone.com//as/jwks" + +Globals: + Function: + Timeout: 30 + MemorySize: 256 + Runtime: python3.9 + +Resources: + # API Gateway + SimpleJwtApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Cors: + AllowMethods: "'GET,POST,OPTIONS'" + AllowHeaders: "'Content-Type,Authorization'" + AllowOrigin: "'*'" + Auth: + DefaultAuthorizer: JwtAuthorizer + Authorizers: + JwtAuthorizer: + FunctionArn: !GetAtt JwtAuthorizerFunction.Arn + + # JWT Authorizer Function + JwtAuthorizerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: jwt_authorizer.lambda_handler + Environment: + Variables: + PING_ISSUER_URL: !Ref PingOneIssuer + PING_JWKS_URL: !Ref PingOneJwksUrl + + # User Info Handler Function + UserInfoFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: user_info.lambda_handler + Events: + GetUserInfo: + Type: Api + Properties: + RestApiId: !Ref SimpleJwtApi + Path: /user + Method: get + +Outputs: + UserInfoEndpoint: + Description: User info endpoint + Value: !Sub "https://${SimpleJwtApi}.execute-api.${AWS::Region}.amazonaws.com/prod/user"