Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT Signing #212

Merged
merged 5 commits into from
Nov 1, 2024
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
84 changes: 84 additions & 0 deletions edgecompute/examples/authentication/jwt-signing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# JWT Signing

_Keyword(s):_ jwt, authentication<br>

## Copyright Notice

(c) Copyright 2024 Akamai Technologies, Inc. Licensed under Apache 2 license.

This project provides an Akamai Edgeworker solution for signing JSON Web Tokens (JWTs) and attaching them to API requests. It includes an example of how to sign a JWT using HMAC-SHA256 and then add the JWT and an API key to the headers of an outgoing request.

## Security Considerations

> [!IMPORTANT]
> Secrets are stored in [Property Manager variables](https://techdocs.akamai.com/property-mgr/docs/user-defined-vars). Property Manager variables should be marked as hidden to prevent exposure with akamai-x-get-extracted-values. Depending on your security considerations, you may wish to store secrets elsewhere or use asymmetric keys.

## Usage Example

> [!NOTE]
> This EdgeWorker requires you to create a Property Manager variable named `PMUSER_JWT_HMAC_KEY` (if you prefer something different, ensure that the reference in the Edgeworker is updated), set it to hidden, and then paste an encoded key into it.

If you do not yet have an HMAC key, an example for doing so can be done by running the following in your browser:

```
const key = await crypto.subtle.generateKey(
{
name: "HMAC",
hash: { name: "SHA-256" },
},
true, // Key must be extractable to export
["sign", "verify"]
);

// To export the key in "raw" format
const rawKey = await crypto.subtle.exportKey("raw", key);
console.log("Raw HMAC key:", new Uint8Array(rawKey));

// To export the key in "jwk" format
const jwkKey = await crypto.subtle.exportKey("jwk", key);
console.log("JWK HMAC key:", JSON.stringify(jwkKey));
```

### Explanation

1. **Raw Key**:

- `raw` format returns the binary representation of the key.
- The `Uint8Array` wrapper helps display the raw key as an array of bytes, which is useful for debugging.
- Console Output for `raw` key:

```
Raw HMAC key: Uint8Array(32) [24, 134, 239, 140, ...] // Array of bytes
```

2. **JWK Key**:

- `jwk` format returns a JSON Web Key, a JSON object that includes key details.
- This format is more readable and interoperable, especially when working with web APIs.
- Console Output for `jwk` key:

```
JWK HMAC key: {
"kty": "oct",
"k": "SGVsbG9Xb3JsZEtleQ...", // Base64 URL encoded key
"alg": "HS256",
"ext": true
}
```

In the JWK output:

- `kty`: Key type, "oct" for symmetric keys.
- `k`: The actual key material, base64url encoded.
- `alg`: Algorithm, here `"HS256"` for HMAC-SHA-256.
- `ext`: Indicates if the key is extractable (`true` in this case).

Take note that the main.js version uses `raw` key format. If using `jwk` in the example above, the stringified console output of `jwkKey` above may be pasted directly into the Property Manager variable `PMUSER_JWT_HMAC_KEY`.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great explanation! Thanks!

## Resources

For more information on JWT Module, please refer to the following resources:

- [JWT API Documentation](https://techdocs.akamai.com/edgeworkers/docs/jwt)
- [Crypto module documentation](https://techdocs.akamai.com/edgeworkers/docs/crypto)
- See the repo [README](https://github.com/akamai/edgeworkers-examples#Resources) for additional guidance.
4 changes: 4 additions & 0 deletions edgecompute/examples/authentication/jwt-signing/bundle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"edgeworker-version": "0.1",
"description": "Generates a signed JWT for origin verification."
}
88 changes: 88 additions & 0 deletions edgecompute/examples/authentication/jwt-signing/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
(c) Copyright 2024 Akamai Technologies, Inc. Licensed under Apache 2 license.

Version: 1.0
Purpose: EdgeWorker that signs a JWT for origin validation by leveraging the Web Crypto API

*/

import { logger } from 'log';
evan-hughes marked this conversation as resolved.
Show resolved Hide resolved
import { crypto } from 'crypto';
import { TextEncoder, base64url } from 'encoding';

async function signJWT(payload, secret) {
logger.log('Start: Signing JWT');

try {

// Get current time for issued claim
const currentTime = Math.floor(Date.now() / 1000);

// Set payload 'issued at' (iat) and 'expiration' (exp) claims
payload.iat = currentTime;
payload.exp = currentTime + 300; // Expires in 5 minutes

// JWT header
const header = {
alg: 'HS256'
, typ: 'JWT'
};

// Encode header and payload in base64URL for JWT
const encoder = new TextEncoder();
const encodedHeader = base64url.encode(encoder.encode(JSON.stringify(header)));
const encodedPayload = base64url.encode(encoder.encode(JSON.stringify(payload)));
const message = `${encodedHeader}.${encodedPayload}`;

// Import the secret key for HMAC signing
const keyData = new Uint8Array(encoder.encode(secret));
const cryptoKey = await crypto.subtle.importKey(
'raw'
, keyData.buffer
, { name: 'HMAC', hash: 'SHA-256' }
, false
, ['sign']
);

// Sign the JWT
const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
const encodedSignature = base64url.encode(new Uint8Array(signature));

// Return the complete JWT
return `${message}.${encodedSignature}`;

} catch (error) {
logger.error('Error during JWT signing', error);

return error.message;
}
}

export async function onOriginRequest(request) {
logger.log('Start: onOriginRequest');

try {
// Retrieve secrets from environment
const secretKey = request.getVariable('PMUSER_JWT_HMAC_KEY');
const apiKey = request.getVariable('PMUSER_CSS_API_KEY');

// Prepare JWT payload
const payload = {
sub: apiKey
, iss: 'issuer-string-here'
};

// Generate the JWT
const jwt = await signJWT(payload, secretKey);

// Modify outbound request headers
request.removeHeader('Transfer-Encoding');
request.addHeader('X-API-KEY', apiKey);
request.addHeader('X-JWT', `Bearer ${jwt}`);

} catch (error) {
logger.error('Error in onOriginRequest', error);

throw error;
}
}