Header menu logo FsCDK

OAuth 2.0 Machine-to-Machine Authentication with Cognito

Implement secure service-to-service authentication using Cognito User Pools, OAuth 2.0 scopes, and Lambda authorizers.

OAuth 2.0 M2M Flow

OAuth 2.0 Machine-to-Machine Authentication Flow

Architecture Overview

Traditional User Auth: User → Cognito → JWT → API Gateway → Lambda M2M Auth: Service → OAuth Client Credentials → Access Token → API Gateway → Lambda

OAuth Resource Server with Custom Scopes

Define business-specific scopes for your API:

#r "../src/bin/Release/net8.0/publish/Amazon.JSII.Runtime.dll"
#r "../src/bin/Release/net8.0/publish/Constructs.dll"
#r "../src/bin/Release/net8.0/publish/Amazon.CDK.Lib.dll"
#r "../src/bin/Release/net8.0/publish/FsCDK.dll"

open FsCDK
open Amazon.CDK
open Amazon.CDK.AWS.Cognito
open Amazon.CDK.AWS.Lambda
open Amazon.CDK.AWS.IAM

Basic M2M Setup

Create a Cognito User Pool with OAuth resource server for machine-to-machine authentication:

stack "M2MAuth" {
    // User Pool for both user and M2M authentication
    let! myUserPool =
        userPool "AppUserPool" {
            signInWithEmail
            selfSignUpEnabled false // M2M clients created administratively
        }

    // Define OAuth resource server with custom scopes
    userPoolResourceServer "ApiResourceServer" {
        userPool myUserPool
        identifier "api"
        name "API Resource Server"
        scope "read" "Read access to resources"
        scope "write" "Write access to resources"
        scope "admin" "Administrative operations"
    }

    // Create M2M client
    userPoolClient "ServiceClient" {
        userPool myUserPool
        generateSecret true // Required for client credentials flow

        authFlows (
            AuthFlow(
                UserSrp = false,
                UserPassword = false,
                AdminUserPassword = false
            // Custom = true // For client_credentials grant
            )
        )

        // Short-lived tokens for services
        tokenValidities (
            (Duration.Days 30.0), // refreshToken (not used in client_credentials)
            (Duration.Hours 1.0), // accessToken
            (Duration.Hours 1.0) // idToken
        )
    }
}

Resource Server with Multiple Scopes

Add more scopes to support different permission levels:

stack "CompleteM2MOAuth" {
    let! myUserPool =
        userPool "AppUserPool" {
            signInWithEmail
            selfSignUpEnabled false
        }

    // Resource server with granular scopes
    userPoolResourceServer "ApiResourceServer" {
        userPool myUserPool
        identifier "api"
        scope "read" "Read access to resources"
        scope "write" "Write access to resources"
        scope "delete" "Delete access to resources"
        scope "admin" "Administrative operations"
        scope "execute" "Execute business transactions"
    }
}

Lambda Authorizer for Dual Authentication

Support both user JWT tokens and M2M access tokens:

stack "DualAuthAPI" {
    // Lambda authorizer supporting both token types
    lambda "ApiAuthorizer" {
        runtime Runtime.PYTHON_3_11
        handler "authorizer.handler"
        code "./authorizer"
        timeout 30.0

        environment [ "USER_POOL_ID", "us-east-1_XXXXX"; "REGION", "us-east-1" ]

        // IAM permissions for Cognito
        addRolePolicyStatement (
            policyStatement {
                effect Effect.ALLOW
                actions [ "cognito-idp:DescribeUserPool" ]
                resources [ "arn:aws:cognito-idp:us-east-1:123456789012:userpool/*" ]
            }
        )
    }
}

Authorizer Implementation Pattern

Lambda authorizer implementation to handle both user JWT and M2M access tokens.

Note: Python example shown below as Lambda authorizers commonly use Python/Node.js for JWT validation. The FsCDK builder above shows how to deploy this Lambda function.

import jwt
import requests
from typing import Dict, Any

def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]:
    token = extract_token(event)

    # Decode without verification to check token type
    unverified = jwt.decode(token, options={"verify_signature": False})

    if unverified.get('token_use') == 'access':
        # M2M access token flow
        claims = validate_access_token(token)
        context = extract_m2m_context(claims)
    elif unverified.get('token_use') == 'id':
        # User JWT token flow
        claims = validate_id_token(token)
        context = extract_user_context(claims)
    else:
        raise Exception('Unknown token type')

    return generate_policy(context)

def extract_m2m_context(claims):
    """Extract tenant and permissions from M2M token"""
    client_id = claims['client_id']
    scopes = claims.get('scope', '').split()

    # Map scopes to application roles
    roles = []
    if 'api/admin' in scopes:
        roles.append('admin')
    if 'api/write' in scopes:
        roles.append('write')
    if 'api/read' in scopes:
        roles.append('read')

    return {
        'clientId': client_id,
        'roles': ','.join(roles),
        'authType': 'm2m'
    }

Obtaining M2M Access Tokens

Services request tokens using client credentials:

# Token endpoint
curl -X POST https://mydomain.auth.us-east-1.amazoncognito.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=api/read api/write"

# Response
{
  "access_token": "eyJraWQiOiJ...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Using M2M Tokens

# Call API with M2M token
curl https://api.example.com/resources \
  -H "Authorization: Bearer eyJraWQiOiJ..."

Scope-to-Role Mapping Strategy

Map OAuth scopes to application-specific business roles:

OAuth Scope

Application Role

Permissions

api/read

viewer

GET operations only

api/write

editor

GET, POST, PUT operations

api/admin

administrator

All operations including DELETE

api/execute

executor

Execute business transactions

Token Validation Best Practices

def validate_access_token(token):
    """Validate M2M access token with Cognito"""
    # Download Cognito JWKS
    jwks_url = f'https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/jwks.json'
    jwks = requests.get(jwks_url).json()

    # Validate signature, issuer, expiration
    claims = jwt.decode(
        token,
        jwks,
        algorithms=['RS256'],
        issuer=f'https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}'
    )

    # Verify token_use
    if claims.get('token_use') != 'access':
        raise Exception('Invalid token type')

    # Verify not expired
    if claims['exp'] < time.time():
        raise Exception('Token expired')

    return claims

Security Considerations

M2M Client Management

Token Security

API Gateway Integration

// API Gateway method with authorizer
// (Configuration varies by gateway type - REST vs HTTP)

Cost Considerations

Cognito M2M pricing:

Monitoring M2M Authentication

CloudWatch metrics to track:

Complete M2M Example

// 1. Create User Pool with resource server (see above)
// 2. Create M2M client with client_credentials flow
// 3. Deploy Lambda authorizer
// 4. Configure API Gateway to use authorizer
// 5. Services request tokens and call API

Migration from API Keys

If migrating from API key authentication:

API Keys

OAuth M2M

Manual rotation

Automatic expiration

Broad access

Granular scopes

Simple but risky

Secure but complex

No usage attribution

Client-level tracking

Migration strategy: 1. Implement M2M alongside API keys 2. Migrate services one-by-one 3. Monitor for 30 days 4. Deprecate API keys

Resources

namespace FsCDK
namespace Amazon
namespace Amazon.CDK
namespace Amazon.CDK.AWS
namespace Amazon.CDK.AWS.Cognito
namespace Amazon.CDK.AWS.Lambda
namespace Amazon.CDK.AWS.IAM
val stack: name: string -> StackBuilder
<summary>Creates an AWS CDK Stack construct.</summary>
<param name="name">The name of the stack.</param>
<code lang="fsharp"> stack "MyStack" { lambda myFunction bucket myBucket } </code>
val myUserPool: IUserPool
val userPool: name: string -> UserPoolBuilder
<summary>Creates a Cognito User Pool with AWS best practices.</summary>
<param name="name">The user pool name.</param>
<code lang="fsharp"> userPool "MyUserPool" { signInWithEmail selfSignUpEnabled true mfa Mfa.OPTIONAL } </code>
custom operation: signInWithEmail Calls UserPoolBuilder.SignInWithEmail
<summary>Enables email only as sign-in alias.</summary>
custom operation: selfSignUpEnabled (bool) Calls UserPoolBuilder.SelfSignUpEnabled
<summary>Enables or disables self sign-up.</summary>
val userPoolResourceServer: name: string -> UserPoolResourceServerBuilder
<summary>Creates a builder for a Cognito User Pool Resource Server.</summary>
<param name="name">The logical name for the resource server.</param>
<code lang="fsharp"> userPoolResourceServer "ApiServer" { userPool myUserPool identifier "api" name "API Resource Server" scope "read" "Read access" } </code>
custom operation: userPool (IUserPool) Calls UserPoolResourceServerBuilder.UserPool
<summary>Sets the user pool associated with this resource server.</summary>
<param name="config">The current resource server configuration.</param>
<param name="pool">The Cognito user pool.</param>
<code lang="fsharp"> userPoolResourceServer "ApiServer" { userPool myUserPool } </code>
custom operation: identifier (string) Calls UserPoolResourceServerBuilder.Identifier
<summary>Sets the identifier used by OAuth2 to reference this resource server.</summary>
<param name="config">The current resource server configuration.</param>
<param name="id">The resource server identifier, e.g., <c>api</c> or <c>com.example.api</c>.</param>
<code lang="fsharp"> userPoolResourceServer "ApiServer" { identifier "api" } </code>
custom operation: name (string) Calls UserPoolResourceServerBuilder.Name
<summary>Sets the display name for the resource server.</summary>
<param name="config">The current resource server configuration.</param>
<param name="name">The resource server name.</param>
<code lang="fsharp"> userPoolResourceServer "ApiServer" { name "API Resource Server" } </code>
custom operation: scope (string) (string) Calls UserPoolResourceServerBuilder.Scope
<summary>Adds a single scope from a name/description tuple.</summary>
<param name="config">The current resource server configuration.</param>
<param name="name">The scope name.</param>
<param name="description">The scope description.</param>
<code lang="fsharp"> userPoolResourceServer "ApiServer" { scope "admin" "Admin access" } </code>
val userPoolClient: name: string -> UserPoolClientBuilder
<summary>Creates a Cognito User Pool Client.</summary>
<param name="name">The client name.</param>
<code lang="fsharp"> userPoolClient "MyAppClient" { userPool myUserPool generateSecret false } </code>
custom operation: userPool (IUserPool) Calls UserPoolClientBuilder.UserPool
<summary>Sets the user pool.</summary>
custom operation: generateSecret (bool) Calls UserPoolClientBuilder.GenerateSecret
<summary>Enables or disables secret generation.</summary>
custom operation: authFlows (IAuthFlow) Calls UserPoolClientBuilder.AuthFlows
<summary>Sets authentication flows.</summary>
Multiple items
type AuthFlow = interface IAuthFlow new: unit -> unit member AdminUserPassword: Nullable<bool> member Custom: Nullable<bool> member User: Nullable<bool> member UserPassword: Nullable<bool> member UserSrp: Nullable<bool>

--------------------
AuthFlow() : AuthFlow
custom operation: tokenValidities (Duration * Duration * Duration) Calls UserPoolClientBuilder.TokenValidities
<summary>Sets token validities.</summary>
type Duration = inherit DeputyBase member FormatTokenToNumber: unit -> string member IsUnresolved: unit -> bool member Minus: rhs: Duration -> Duration member Plus: rhs: Duration -> Duration member ToDays: ?opts: ITimeConversionOptions -> float member ToHours: ?opts: ITimeConversionOptions -> float member ToHumanString: unit -> string member ToIsoString: unit -> string member ToMilliseconds: ?opts: ITimeConversionOptions -> float ...
Duration.Days(amount: float) : Duration
Duration.Hours(amount: float) : Duration
val lambda: name: string -> FunctionBuilder
<summary>Creates a Lambda function configuration.</summary>
<param name="name">The function name.</param>
<code lang="fsharp"> lambda "MyFunction" { handler "index.handler" runtime Runtime.NODEJS_18_X code "./lambda" timeout 30.0 } </code>
custom operation: runtime (Runtime) Calls FunctionBuilder.Runtime
<summary>Sets the runtime for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="runtime">The Lambda runtime.</param>
<code lang="fsharp"> lambda "MyFunction" { runtime Runtime.NODEJS_18_X } </code>
Multiple items
type Runtime = inherit DeputyBase new: name: string * ?family: Nullable<RuntimeFamily> * ?props: ILambdaRuntimeProps -> unit member RuntimeEquals: other: Runtime -> bool member ToString: unit -> string member BundlingImage: DockerImage member Family: Nullable<RuntimeFamily> member IsVariable: bool member Name: string member SupportsCodeGuruProfiling: bool member SupportsInlineCode: bool ...

--------------------
Runtime(name: string, ?family: System.Nullable<RuntimeFamily>, ?props: ILambdaRuntimeProps) : Runtime
property Runtime.PYTHON_3_11: Runtime with get
custom operation: handler (string) Calls FunctionBuilder.Handler
<summary>Sets the handler for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="handler">The handler name (e.g., "index.handler").</param>
<code lang="fsharp"> lambda "MyFunction" { handler "index.handler" } </code>
custom operation: code (Code) Calls FunctionBuilder.Code
<summary>Sets the code source from a Code object.</summary>
<param name="config">The function configuration.</param>
<param name="path">The Code object.</param>
<code lang="fsharp"> lambda "MyFunction" { code (Code.FromBucket myBucket "lambda.zip") } </code>
custom operation: timeout (float) Calls FunctionBuilder.Timeout
<summary>Sets the timeout for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="seconds">The timeout in seconds.</param>
<code lang="fsharp"> lambda "MyFunction" { timeout 30.0 } </code>
custom operation: environment ((string * string) list) Calls FunctionBuilder.Environment
<summary>Sets environment variables for the Lambda function.</summary>
<param name="config">The function configuration.</param>
<param name="env">List of key-value pairs for environment variables.</param>
<code lang="fsharp"> lambda "MyFunction" { environment [ "KEY1", "value1"; "KEY2", "value2" ] } </code>
custom operation: addRolePolicyStatement (PolicyStatement) Calls FunctionBuilder.AddRolePolicyStatement
<summary>Adds a single role policy statement.</summary>
<param name="config">The function configuration.</param>
<param name="statements">Policy statement.</param>
<code lang="fsharp"> lambda "MyFunction" { addRolePolicyStatement stmt } </code>
val policyStatement: PolicyStatementBuilder
custom operation: effect (Effect) Calls PolicyStatementBuilder.Effect
[<Struct>] type Effect = | ALLOW = 0 | DENY = 1
field Effect.ALLOW: Effect = 0
custom operation: actions (string list) Calls PolicyStatementBuilder.Actions
custom operation: resources (string list) Calls PolicyStatementBuilder.Resources

Type something to start searching.