Appearance
Authentication
StoreFront integrates with your authentication system to ensure only your intended audience can access your gift card store. This allows you to lock down access to employees, members, or customers based on your existing identity infrastructure.
Use Mature Libraries
If you're implementing authentication yourself, we strongly recommend using well-established, battle-tested libraries for your chosen protocol. Mature libraries handle edge cases, security considerations, and protocol nuances that are easy to overlook in custom implementations - and can significantly reduce your integration effort.
StoreFront supports the following authentication methods:
| Method | Best For | Complexity |
|---|---|---|
| Third-Party IdP | Buyers using Auth0, Okta, Azure AD, etc. | Low |
| OAuth 2.0 | Custom OAuth implementations | Medium |
| OpenID Connect (OIDC) | Custom OIDC implementations | Medium |
| JWT | Minimal integration, direct token exchange | Low to Medium |
Using a Third-Party Identity Provider
Identity-as-a-Service providers like Auth0, Okta, Azure AD, and Google Identity handle authentication for you - security, MFA, user management, and scaling are all managed. If you already use one, connecting to StoreFront is straightforward. If you're building from scratch, they're worth considering for these same reasons.
Setup
Setup takes just a few minutes on each side:
In your IdP:
- Create a new application for StoreFront
- Paste in the callback URL we provide (e.g.,
https://your.storefront/callback) - Copy your Client ID, Client Secret, and Discovery URL
Send to Tillo:
| Setting | Example |
|---|---|
| Client ID | abc123def456 |
| Client Secret | secret_xyz789 |
| Discovery URL | https://your-tenant.auth0.com/.well-known/openid-configuration |
That's it - we handle the rest. Users are redirected to your IdP's login page, authenticate there, and are returned to StoreFront with a valid session.
JWT Authentication
JWT (JSON Web Token) authentication is a simple, direct method where of authentication where your system generates a signed token containing user information. This is ideal when you want minimal integration complexity or are building a custom authentication flow.
How It Works
null
Key Verification Methods
StoreFront supports three methods for verifying JWT signatures, each with different security and operational characteristics:
1. Asymmetric Keys with JWKS (Recommended)
Your system hosts a JWKS (JSON Web Key Set) endpoint containing your public keys. StoreFront fetches keys automatically and handles rotation seamlessly.
Security: Highest
Key Rotation: Self-service, no coordination required
Your Requirements:
- Generate an RSA or EC key pair
- Host a JWKS endpoint (e.g.,
https://auth.your-company.com/.well-known/jwks.json) - Sign JWTs with your private key
- Include the
kid(key ID) in JWT headers
JWKS Endpoint Format:
json
{
"keys": [
{
"kty": "RSA",
"kid": "key-2024-01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuu...",
"e": "AQAB"
}
]
}{
"keys": [
{
"kty": "RSA",
"kid": "key-2024-01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuu...",
"e": "AQAB"
}
]
}Key Rotation: Add a new key to your JWKS, start signing with the new key, then remove the old key after a transition period.
Generating Key Pairs
Use OpenSSL to generate an RSA key pair for use with RS256:
bash
# Generate a 2048-bit RSA private key
openssl genrsa -out private-key.pem 2048
# Extract the public key
openssl rsa -in private-key.pem -pubout -out public-key.pem# Generate a 2048-bit RSA private key
openssl genrsa -out private-key.pem 2048
# Extract the public key
openssl rsa -in private-key.pem -pubout -out public-key.pemAlgorithm Options
RS256 with a 2048-bit key provides strong security for most use cases. If your security requirements demand it, RS384 and RS512 are also supported, as are elliptic curve algorithms (ES256, ES384). For RS512, consider using a 4096-bit key for optimal security matching.
Protect Your Private Keys
- Never commit private keys to version control
- Use secrets management (AWS Secrets Manager, HashiCorp Vault, etc.)
Building a JWKS Endpoint
null
Your JWKS endpoint must return a JSON document containing your public keys in JWK (JSON Web Key) format.
Code Examples - For Guidance Only
The following code examples are provided for illustrative purposes only and are not intended for production use. You should:
- Use well-established, actively maintained libraries for JWK generation and JWKS hosting
- Refer to RFC 7517 (JSON Web Key) for the authoritative specification
- Conduct thorough security review and testing before deploying to production
- Consider using existing JWKS solutions from your identity provider or framework
javascript
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
// Load your public key(s)
const publicKeyPem = fs.readFileSync('public-key.pem', 'utf8');
// Convert PEM to JWK format
function pemToJwk(pem, kid, alg) {
const key = crypto.createPublicKey(pem);
const jwk = key.export({ format: 'jwk' });
return {
...jwk,
kid: kid,
use: 'sig',
alg: alg
};
}
// JWKS endpoint
app.get('/.well-known/jwks.json', (req, res) => {
const jwks = {
keys: [
pemToJwk(publicKeyPem, 'key-2024-01', 'RS256'),
// Add more keys here for rotation
]
};
res.json(jwks);
});
app.listen(3000);const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
// Load your public key(s)
const publicKeyPem = fs.readFileSync('public-key.pem', 'utf8');
// Convert PEM to JWK format
function pemToJwk(pem, kid, alg) {
const key = crypto.createPublicKey(pem);
const jwk = key.export({ format: 'jwk' });
return {
...jwk,
kid: kid,
use: 'sig',
alg: alg
};
}
// JWKS endpoint
app.get('/.well-known/jwks.json', (req, res) => {
const jwks = {
keys: [
pemToJwk(publicKeyPem, 'key-2024-01', 'RS256'),
// Add more keys here for rotation
]
};
res.json(jwks);
});
app.listen(3000);python
from flask import Flask, jsonify
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64
import json
app = Flask(__name__)
def pem_to_jwk(pem_path, kid, alg):
"""Convert a PEM public key to JWK format."""
with open(pem_path, 'rb') as f:
public_key = serialization.load_pem_public_key(f.read(), backend=default_backend())
# Get the public numbers
numbers = public_key.public_numbers()
# RSA key
if hasattr(numbers, 'n'):
def int_to_base64url(n):
byte_length = (n.bit_length() + 7) // 8
return base64.urlsafe_b64encode(
n.to_bytes(byte_length, 'big')
).rstrip(b'=').decode('ascii')
return {
'kty': 'RSA',
'kid': kid,
'use': 'sig',
'alg': alg,
'n': int_to_base64url(numbers.n),
'e': int_to_base64url(numbers.e)
}
# EC key
else:
curve_name = public_key.curve.name
crv = {'secp256r1': 'P-256', 'secp384r1': 'P-384'}[curve_name]
coord_size = {'P-256': 32, 'P-384': 48}[crv]
def int_to_base64url(n, size):
return base64.urlsafe_b64encode(
n.to_bytes(size, 'big')
).rstrip(b'=').decode('ascii')
return {
'kty': 'EC',
'kid': kid,
'use': 'sig',
'alg': alg,
'crv': crv,
'x': int_to_base64url(numbers.x, coord_size),
'y': int_to_base64url(numbers.y, coord_size)
}
@app.route('/.well-known/jwks.json')
def jwks():
keys = [
pem_to_jwk('public-key.pem', 'key-2024-01', 'RS256'),
# Add more keys here for rotation
]
return jsonify({'keys': keys})
if __name__ == '__main__':
app.run(port=3000)from flask import Flask, jsonify
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64
import json
app = Flask(__name__)
def pem_to_jwk(pem_path, kid, alg):
"""Convert a PEM public key to JWK format."""
with open(pem_path, 'rb') as f:
public_key = serialization.load_pem_public_key(f.read(), backend=default_backend())
# Get the public numbers
numbers = public_key.public_numbers()
# RSA key
if hasattr(numbers, 'n'):
def int_to_base64url(n):
byte_length = (n.bit_length() + 7) // 8
return base64.urlsafe_b64encode(
n.to_bytes(byte_length, 'big')
).rstrip(b'=').decode('ascii')
return {
'kty': 'RSA',
'kid': kid,
'use': 'sig',
'alg': alg,
'n': int_to_base64url(numbers.n),
'e': int_to_base64url(numbers.e)
}
# EC key
else:
curve_name = public_key.curve.name
crv = {'secp256r1': 'P-256', 'secp384r1': 'P-384'}[curve_name]
coord_size = {'P-256': 32, 'P-384': 48}[crv]
def int_to_base64url(n, size):
return base64.urlsafe_b64encode(
n.to_bytes(size, 'big')
).rstrip(b'=').decode('ascii')
return {
'kty': 'EC',
'kid': kid,
'use': 'sig',
'alg': alg,
'crv': crv,
'x': int_to_base64url(numbers.x, coord_size),
'y': int_to_base64url(numbers.y, coord_size)
}
@app.route('/.well-known/jwks.json')
def jwks():
keys = [
pem_to_jwk('public-key.pem', 'key-2024-01', 'RS256'),
# Add more keys here for rotation
]
return jsonify({'keys': keys})
if __name__ == '__main__':
app.run(port=3000)php
<?php
// Using web-token/jwt-key-mgmt for JWK conversion
// composer require web-token/jwt-key-mgmt
use Jose\Component\Core\JWK;
use Jose\Component\KeyManagement\JWKFactory;
// Convert PEM to JWK
function pemToJwk(string $pemPath, string $kid, string $alg): array {
$pem = file_get_contents($pemPath);
$jwk = JWKFactory::createFromKey($pem);
return array_merge($jwk->all(), [
'kid' => $kid,
'use' => 'sig',
'alg' => $alg
]);
}
// Simple JWKS endpoint (e.g., in a Laravel route or standalone script)
header('Content-Type: application/json');
$jwks = [
'keys' => [
pemToJwk('public-key.pem', 'key-2024-01', 'RS256'),
// Add more keys here for rotation
]
];
echo json_encode($jwks);<?php
// Using web-token/jwt-key-mgmt for JWK conversion
// composer require web-token/jwt-key-mgmt
use Jose\Component\Core\JWK;
use Jose\Component\KeyManagement\JWKFactory;
// Convert PEM to JWK
function pemToJwk(string $pemPath, string $kid, string $alg): array {
$pem = file_get_contents($pemPath);
$jwk = JWKFactory::createFromKey($pem);
return array_merge($jwk->all(), [
'kid' => $kid,
'use' => 'sig',
'alg' => $alg
]);
}
// Simple JWKS endpoint (e.g., in a Laravel route or standalone script)
header('Content-Type: application/json');
$jwks = [
'keys' => [
pemToJwk('public-key.pem', 'key-2024-01', 'RS256'),
// Add more keys here for rotation
]
];
echo json_encode($jwks);JWKS Response Example (RSA)
json
{
"keys": [
{
"kty": "RSA",
"kid": "key-2024-01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt...",
"e": "AQAB"
}
]
}{
"keys": [
{
"kty": "RSA",
"kid": "key-2024-01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt...",
"e": "AQAB"
}
]
}In our JWT flow, the entire authentication journey looks like the following with the JWKS flow added:
null
EC Keys
If using elliptic curve algorithms (ES256, ES384), the JWK format differs - see RFC 7518 Section 6.2 for the EC key format specification.
Caching Considerations
StoreFront caches JWKS responses for performance. When rotating keys:
- Add the new key to your JWKS
- Start signing with the new key
- StoreFront will retry validation with a fresh JWKS fetch automatically if a key cannot be resolved or token validation fails
- Remove the old key after a grace period
StoreFront caches JWKS responses for up to 1 hour, but key rotation does not require waiting for cache expiry.
2. Asymmetric Keys with Upload
You generate a key pair and upload the public key to StoreFront. This is suitable when you cannot host a JWKS endpoint.
Security: High
Key Rotation: Requires coordination with Tillo
Your Requirements:
- Generate an RSA or EC key pair
- Provide the public key to Tillo during setup
- Sign JWTs with your private key
- Include the
kid(key ID) provided by Tillo in JWT headers
Key Rotation: Contact Tillo to upload a new public key. Both keys can be active during the transition period.
3. Symmetric Keys (HMAC Shared Secret)
Security: Moderate
Key Rotation: Requires coordination with Tillo
A shared secret is generated by StoreFront and provided to you. Both parties use the same secret for signing and verification.
Security Consideration
Symmetric keys have a wider attack surface because the same secret is used for both signing and verification. If your system is compromised, an attacker could forge tokens. Use asymmetric keys when possible.
Your Requirements:
- Receive the shared secret from Tillo during setup
- Sign JWTs using HS256
- Include the
kid(key ID) provided by Tillo in JWT headers - Store the secret securely - never expose it in client-side code
Algorithm Options
HS256 provides strong security for most use cases. HS384 and HS512 are also available if your security requirements demand stronger hashing.
Key Rotation: Contact Tillo to generate a new secret. A grace period allows both secrets to be valid during transition.
JWT Structure
A JWT consists of three parts: header, payload, and signature.
Header
json
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2024-01"
}{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2024-01"
}| Field | Description |
|---|---|
alg | Signing algorithm (e.g., RS256 for asymmetric, HS256 for symmetric) |
typ | Token type, always JWT |
kid | Key ID for key lookup (required) |
Payload (Claims)
json
{
"iss": "urn:tillo:storefront:production:buyer:acme-corp",
"aud": "urn:tillo:storefront:production:buyer:acme-corp",
"sub": "user-12345",
"exp": 1735689600,
"iat": 1735689300,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "user@example.com"
}{
"iss": "urn:tillo:storefront:production:buyer:acme-corp",
"aud": "urn:tillo:storefront:production:buyer:acme-corp",
"sub": "user-12345",
"exp": 1735689600,
"iat": 1735689300,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "user@example.com"
}| Claim | Required | Description | Provided By |
|---|---|---|---|
iss | Yes | Issuer identifier - must match the value agreed during setup | Agreed during setup |
aud | Yes | Audience identifier - must match the value agreed during setup | Agreed during setup |
sub | Yes | Subject - unique user identifier | You (Buyer) |
exp | Yes | Expiration time (Unix timestamp) | You (Buyer) |
iat | Yes | Issued at time (Unix timestamp) | You (Buyer) |
jti | Configurable | JWT ID for replay protection (UUID v4 or base64url, 22-128 chars) | You (Buyer) |
name | Optional | User's display name - used to pre-populate checkout | You (Buyer) |
email | Optional | User's email address - used to pre-populate checkout | You (Buyer) |
Issuer and Audience Format:
Both iss and aud use the same URN format to prevent cross-system/environment token reuse:
urn:tillo:storefront:{environment}:buyer:{you-acme-ltd}urn:tillo:storefront:{environment}:buyer:{you-acme-ltd}For example: urn:tillo:storefront:production:buyer:acme-corp
The exact values will be provided during setup and must match exactly in your JWT.
Token Delivery Methods
StoreFront strictly enforces the configured delivery method. Tokens sent via the wrong method will be rejected.
POST Delivery (Recommended)
The JWT is sent in the POST body to StoreFront's callback endpoint:
http
POST /callback HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id_token=eyJhbGciOiJSUzI1NiIs...POST /callback HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id_token=eyJhbGciOiJSUzI1NiIs...| Requirement | Value |
|---|---|
| Method | POST |
| Content-Type | application/x-www-form-urlencoded |
| Field name | id_token |
| Endpoint | /callback |
This is more secure as tokens are not exposed in URLs, browser history, or server logs.
Optional Claims with POST
When using POST delivery, you can safely include name and email claims in your JWT to pre-populate the checkout page for a smoother user experience. These fields are optional but provide convenience for users.
URL Delivery (Discouraged)
The JWT is appended as a query parameter:
https://your.storefront/callback?token=eyJhbGciOiJSUzI1NiIs...https://your.storefront/callback?token=eyJhbGciOiJSUzI1NiIs...Critical Security Warning - URL Delivery Risks
URL delivery exposes your entire JWT to multiple attack vectors:
- Browser history - Token persists indefinitely on user's device
- Server access logs - Token recorded in your servers, proxies, CDNs, and WAFs
- Referer headers - Token leaked to any third-party resources on the page
- Shoulder surfing - Token visible in address bar
- Shared computers - Token accessible to subsequent users
If you must use URL delivery:
- NEVER include
name,email, or any personally identifiable information - Keep token expiry as short as possible
- StoreFront enforces replay protection by validating the
jticlaim - Consider the regulatory implications (GDPR, CCPA) of token exposure
For new integrations, always use POST delivery.
Security Features
Replay Protection (JTI)
When enabled, each JWT can only be used once. StoreFront tracks the jti claim and rejects any token that has already been used, preventing replay attacks where an attacker intercepts and reuses a valid token.
How it works:
- You generate a unique
jtifor each token - StoreFront records the
jtiwhen the token is first used - Any subsequent use of the same
jtiis rejected - The
jtiis tracked until the token'sexptime passes
JTI Requirements:
- Must be cryptographically random and unpredictable
- Accepted formats:
- UUID v4 with hyphens:
550e8400-e29b-41d4-a716-446655440000 - UUID v4 without hyphens:
550e8400e29b41d4a716446655440000 - Base64url string: 22-128 characters using
[A-Za-z0-9_-]
- UUID v4 with hyphens:
- Must have high entropy - do not use sequential numbers, timestamps, or predictable patterns
Token Freshness
StoreFront validates that tokens are fresh and not stale:
exp: Token must not be expired (with configurable clock skew tolerance, default: 30 seconds)iat: Token must not exceed the maximum age from issuance (default: 60 seconds)
Why 60 seconds?
JWTs for StoreFront are login assertions, not access tokens. They should be generated just-in-time and used immediately. The short maximum age prevents tokens from being intercepted and used later.
Issuer and Audience Validation
issandaud: Must match the values provided during setup
Example Implementation
Code Examples - For Guidance Only
The following code examples are provided for illustrative purposes only and are not intended for production use without modification. You should:
- Use well-established JWT libraries appropriate for your language/framework
- Refer to RFC 7519 (JWT) and RFC 7515 (JWS) for authoritative specifications
- Conduct thorough security review and testing before deploying to production
- Ensure your implementation handles errors, edge cases, and key management securely
Here's an example of generating a JWT and sending it to StoreFront via POST:
javascript
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const privateKey = fs.readFileSync('private-key.pem');
// Values provided during setup
const ISS_AUD = 'urn:tillo:storefront:production:buyer:acme-corp';
const STOREFRONT_CALLBACK = 'https://giftcards.acme-corp.com/callback';
function generateToken(userId, userName, userEmail) {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
iss: ISS_AUD,
aud: ISS_AUD,
sub: userId,
name: userName, // Optional - pre-populates checkout
email: userEmail, // Optional - pre-populates checkout
jti: uuidv4(), // Unique token identifier for replay protection
iat: now,
exp: now + 60 // 60 seconds - generate just-in-time
},
privateKey,
{
algorithm: 'RS256',
header: { kid: 'key-2024-01' }
}
);
}
// Send via POST (recommended)
// Return an HTML form that auto-submits to StoreFront
function redirectToStoreFront(token) {
return `
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="${STOREFRONT_CALLBACK}">
<input type="hidden" name="id_token" value="${token}" />
</form>
</body>
</html>
`;
}const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const privateKey = fs.readFileSync('private-key.pem');
// Values provided during setup
const ISS_AUD = 'urn:tillo:storefront:production:buyer:acme-corp';
const STOREFRONT_CALLBACK = 'https://giftcards.acme-corp.com/callback';
function generateToken(userId, userName, userEmail) {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
iss: ISS_AUD,
aud: ISS_AUD,
sub: userId,
name: userName, // Optional - pre-populates checkout
email: userEmail, // Optional - pre-populates checkout
jti: uuidv4(), // Unique token identifier for replay protection
iat: now,
exp: now + 60 // 60 seconds - generate just-in-time
},
privateKey,
{
algorithm: 'RS256',
header: { kid: 'key-2024-01' }
}
);
}
// Send via POST (recommended)
// Return an HTML form that auto-submits to StoreFront
function redirectToStoreFront(token) {
return `
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="${STOREFRONT_CALLBACK}">
<input type="hidden" name="id_token" value="${token}" />
</form>
</body>
</html>
`;
}python
import jwt
import uuid
import time
private_key = open('private-key.pem').read()
# Values provided during setup
ISS_AUD = 'urn:tillo:storefront:production:buyer:acme-corp'
STOREFRONT_CALLBACK = 'https://giftcards.acme-corp.com/callback'
def generate_token(user_id, user_name=None, user_email=None):
now = int(time.time())
payload = {
'iss': ISS_AUD,
'aud': ISS_AUD,
'sub': user_id,
'jti': str(uuid.uuid4()), # Unique token identifier for replay protection
'iat': now,
'exp': now + 60 # 60 seconds - generate just-in-time
}
# Optional claims - only include with POST delivery
if user_name:
payload['name'] = user_name
if user_email:
payload['email'] = user_email
return jwt.encode(
payload,
private_key,
algorithm='RS256',
headers={'kid': 'key-2024-01'}
)
# Send via POST using an HTML form that auto-submits
def redirect_html(token):
return f'''
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="{STOREFRONT_CALLBACK}">
<input type="hidden" name="id_token" value="{token}" />
</form>
</body>
</html>
'''import jwt
import uuid
import time
private_key = open('private-key.pem').read()
# Values provided during setup
ISS_AUD = 'urn:tillo:storefront:production:buyer:acme-corp'
STOREFRONT_CALLBACK = 'https://giftcards.acme-corp.com/callback'
def generate_token(user_id, user_name=None, user_email=None):
now = int(time.time())
payload = {
'iss': ISS_AUD,
'aud': ISS_AUD,
'sub': user_id,
'jti': str(uuid.uuid4()), # Unique token identifier for replay protection
'iat': now,
'exp': now + 60 # 60 seconds - generate just-in-time
}
# Optional claims - only include with POST delivery
if user_name:
payload['name'] = user_name
if user_email:
payload['email'] = user_email
return jwt.encode(
payload,
private_key,
algorithm='RS256',
headers={'kid': 'key-2024-01'}
)
# Send via POST using an HTML form that auto-submits
def redirect_html(token):
return f'''
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="{STOREFRONT_CALLBACK}">
<input type="hidden" name="id_token" value="{token}" />
</form>
</body>
</html>
'''php
use Firebase\JWT\JWT;
use Ramsey\Uuid\Uuid;
$privateKey = file_get_contents('private-key.pem');
// Values provided during setup
const ISS_AUD = 'urn:tillo:storefront:production:buyer:acme-corp';
const STOREFRONT_CALLBACK = 'https://giftcards.acme-corp.com/callback';
function generateToken(string $userId, ?string $userName = null, ?string $userEmail = null): string {
global $privateKey;
$now = time();
$payload = [
'iss' => ISS_AUD,
'aud' => ISS_AUD,
'sub' => $userId,
'jti' => Uuid::uuid4()->toString(), // Unique token identifier for replay protection
'iat' => $now,
'exp' => $now + 60 // 60 seconds - generate just-in-time
];
// Optional claims - only include with POST delivery
if ($userName) {
$payload['name'] = $userName;
}
if ($userEmail) {
$payload['email'] = $userEmail;
}
return JWT::encode($payload, $privateKey, 'RS256', 'key-2024-01');
}
// Send via POST using an HTML form that auto-submits
function redirectHtml(string $token): string {
return <<<HTML
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="STOREFRONT_CALLBACK">
<input type="hidden" name="id_token" value="{$token}" />
</form>
</body>
</html>
HTML;
}use Firebase\JWT\JWT;
use Ramsey\Uuid\Uuid;
$privateKey = file_get_contents('private-key.pem');
// Values provided during setup
const ISS_AUD = 'urn:tillo:storefront:production:buyer:acme-corp';
const STOREFRONT_CALLBACK = 'https://giftcards.acme-corp.com/callback';
function generateToken(string $userId, ?string $userName = null, ?string $userEmail = null): string {
global $privateKey;
$now = time();
$payload = [
'iss' => ISS_AUD,
'aud' => ISS_AUD,
'sub' => $userId,
'jti' => Uuid::uuid4()->toString(), // Unique token identifier for replay protection
'iat' => $now,
'exp' => $now + 60 // 60 seconds - generate just-in-time
];
// Optional claims - only include with POST delivery
if ($userName) {
$payload['name'] = $userName;
}
if ($userEmail) {
$payload['email'] = $userEmail;
}
return JWT::encode($payload, $privateKey, 'RS256', 'key-2024-01');
}
// Send via POST using an HTML form that auto-submits
function redirectHtml(string $token): string {
return <<<HTML
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="STOREFRONT_CALLBACK">
<input type="hidden" name="id_token" value="{$token}" />
</form>
</body>
</html>
HTML;
}OAuth 2.0
OAuth 2.0 is an industry-standard authorisation framework that enables secure, delegated access without sharing credentials.
Mature Server Libraries Available
If you're building your own OAuth 2.0 authorization server, consider using established server-side libraries:
- Node.js: node-oauth2-server or oidc-provider
- Python: Authlib (server mode) or Django OAuth Toolkit
- PHP: league/oauth2-server
- Java: Spring Authorization Server
These handle the protocol complexities and security considerations for you.
These are suggestions based on what was available at the time of writing. You should conduct your own research to find a package that you are comfortable using, trust, and that is actively maintained and recommended by the industry or community.
How It Works
StoreFront uses the Authorization Code Grant flow, which is the most secure OAuth flow for server-side applications:
null
Scopes
OAuth 2.0 doesn't prescribe specific scopes - they're defined by your system. During setup, you'll tell us which scopes to request (e.g., email profile). Custom scopes are supported.
Your Implementation Requirements
You need to provide the following endpoints:
Authorization Endpoint
GET /authorize
StoreFront redirects unauthenticated users here with:
| Parameter | Description |
|---|---|
response_type | Always code |
client_id | Your assigned client ID |
redirect_uri | StoreFront's callback URL |
state | CSRF protection token (must be returned unchanged) |
scope | Requested scopes - configured during setup (e.g., email profile) |
After authenticating the user, redirect back to StoreFront's callback with:
{redirect_uri}?code={authorization_code}&state={state}{redirect_uri}?code={authorization_code}&state={state}Token Endpoint
POST /token
StoreFront exchanges the authorization code for an access token:
json
{
"grant_type": "authorization_code",
"code": "{authorization_code}",
"redirect_uri": "{redirect_uri}",
"client_id": "{client_id}",
"client_secret": "{client_secret}"
}{
"grant_type": "authorization_code",
"code": "{authorization_code}",
"redirect_uri": "{redirect_uri}",
"client_id": "{client_id}",
"client_secret": "{client_secret}"
}Return:
json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}User Info Endpoint (Optional)
GET /userinfo
If configured, StoreFront calls this with the access token to retrieve user details:
Authorization: Bearer {access_token}Authorization: Bearer {access_token}Return user information including a unique identifier:
json
{
"sub": "user-unique-id",
"email": "user@example.com",
"name": "John Doe"
}{
"sub": "user-unique-id",
"email": "user@example.com",
"name": "John Doe"
}Security Considerations
- The authorization code is single-use and short-lived (typically 10 minutes)
- The
stateparameter prevents CSRF attacks - always validate it matches - Token exchange happens server-to-server, never exposing secrets to the browser
PKCE Support (Optional)
StoreFront supports PKCE (Proof Key for Code Exchange) for enhanced security. PKCE protects against authorization code interception attacks by binding the authorization request to the token exchange.
Supported Methods:
| Method | Description | Recommendation |
|---|---|---|
| S256 | SHA-256 hash of the code verifier | Recommended |
| plain | Code verifier sent as-is | Not recommended for production |
How It Works:
- StoreFront generates a random
code_verifier(43-128 characters) - StoreFront creates
code_challenge = BASE64URL(SHA256(code_verifier))(for S256) - The
code_challengeandcode_challenge_methodare sent with the authorization request - Your token endpoint receives the
code_verifierduring code exchange - Your server validates:
BASE64URL(SHA256(code_verifier)) == stored_code_challenge
Authorization Request (with PKCE):
GET /authorize?
response_type=code&
client_id=storefront&
redirect_uri=https://your.storefront/callback&
state=xyz123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256GET /authorize?
response_type=code&
client_id=storefront&
redirect_uri=https://your.storefront/callback&
state=xyz123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256Token Exchange Validation:
Your token endpoint must validate the code_verifier matches the original code_challenge:
Code Examples - For Guidance Only
The following code examples are provided for illustrative purposes only. For production implementations, use your OAuth library's built-in PKCE validation or refer to RFC 7636 (PKCE) for the authoritative specification.
javascript
const crypto = require('crypto');
function validatePkce(codeVerifier, storedCodeChallenge, method) {
if (method === 'plain') {
return codeVerifier === storedCodeChallenge;
}
// S256
const hash = crypto.createHash('sha256').update(codeVerifier).digest();
const computed = hash.toString('base64url');
return computed === storedCodeChallenge;
}const crypto = require('crypto');
function validatePkce(codeVerifier, storedCodeChallenge, method) {
if (method === 'plain') {
return codeVerifier === storedCodeChallenge;
}
// S256
const hash = crypto.createHash('sha256').update(codeVerifier).digest();
const computed = hash.toString('base64url');
return computed === storedCodeChallenge;
}python
import hashlib
import base64
def validate_pkce(code_verifier: str, stored_code_challenge: str, method: str) -> bool:
if method == 'plain':
return code_verifier == stored_code_challenge
# S256
digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
computed = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
return computed == stored_code_challengeimport hashlib
import base64
def validate_pkce(code_verifier: str, stored_code_challenge: str, method: str) -> bool:
if method == 'plain':
return code_verifier == stored_code_challenge
# S256
digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
computed = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
return computed == stored_code_challengephp
function validatePkce(string $codeVerifier, string $storedCodeChallenge, string $method): bool {
if ($method === 'plain') {
return hash_equals($codeVerifier, $storedCodeChallenge);
}
// S256
$hash = hash('sha256', $codeVerifier, true);
$computed = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
return hash_equals($computed, $storedCodeChallenge);
}function validatePkce(string $codeVerifier, string $storedCodeChallenge, string $method): bool {
if ($method === 'plain') {
return hash_equals($codeVerifier, $storedCodeChallenge);
}
// S256
$hash = hash('sha256', $codeVerifier, true);
$computed = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
return hash_equals($computed, $storedCodeChallenge);
}PKCE is recommended for all new OAuth integrations.
OpenID Connect (OIDC)
OpenID Connect is an identity layer built on top of OAuth 2.0. It adds standardised identity claims and discovery mechanisms.
Mature Server Libraries Available
If you're building your own OIDC provider, use server-side libraries that handle the protocol complexities:
- Node.js: oidc-provider - certified OpenID implementation
- Python: Authlib (OIDC server mode)
- PHP: league/oauth2-server with OpenID Connect extension
- Java: Spring Authorization Server
These libraries implement the full OIDC specification including discovery, ID tokens, and JWKS endpoints.
These are suggestions based on what was available at the time of writing. You should conduct your own research to find a package that you are comfortable using, trust, and that is actively maintained and recommended by the industry or community.
Differences from OAuth 2.0
| Feature | OAuth 2.0 | OIDC |
|---|---|---|
| Purpose | Authorization | Authentication + Authorization |
| ID Token | No | Yes (JWT with user claims) |
| Standard Claims | No | Yes (sub, email, name, etc.) |
| Discovery | No | Yes (/.well-known/openid-configuration) |
Scopes
StoreFront sends the openid scope (required for OIDC) along with standard scopes like profile and email. Custom scopes can also be configured during setup.
Your Implementation Requirements
OIDC requires the same endpoints as OAuth 2.0, plus:
Discovery Endpoint (Optional but Recommended)
GET /.well-known/openid-configuration
Returns your OIDC configuration:
json
{
"issuer": "https://auth.your-company.com",
"authorization_endpoint": "https://auth.your-company.com/authorize",
"token_endpoint": "https://auth.your-company.com/token",
"userinfo_endpoint": "https://auth.your-company.com/userinfo",
"jwks_uri": "https://auth.your-company.com/.well-known/jwks.json"
}{
"issuer": "https://auth.your-company.com",
"authorization_endpoint": "https://auth.your-company.com/authorize",
"token_endpoint": "https://auth.your-company.com/token",
"userinfo_endpoint": "https://auth.your-company.com/userinfo",
"jwks_uri": "https://auth.your-company.com/.well-known/jwks.json"
}ID Token
In addition to the access token, your token endpoint should return an ID token:
json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}The ID token is a JWT containing identity claims:
json
{
"iss": "https://auth.your-company.com",
"sub": "user-unique-id",
"aud": "storefront-client-id",
"exp": 1735689600,
"iat": 1735686000,
"email": "user@example.com",
"name": "John Doe"
}{
"iss": "https://auth.your-company.com",
"sub": "user-unique-id",
"aud": "storefront-client-id",
"exp": 1735689600,
"iat": 1735686000,
"email": "user@example.com",
"name": "John Doe"
}