Integrating Replyke with an External User System
When integrating Replyke with an external user system, two primary challenges arise:
- Associating actions and content with external users: Replyke needs to connect its features to users managed in a separate system.
- Ensuring data security: Simply passing user details as a plain object is insecure, as it could be manipulated by malicious actors on the client-side.
To address these challenges, Replyke uses JSON Web Tokens (JWTs) signed with a private key. Below, we outline the logic behind this integration.
Understanding JSON Web Tokens (JWTs)
JWTs are a secure method for transmitting information between two parties. They consist of three parts:
- Header: Contains metadata about the token, such as the signing algorithm.
- Payload: Holds the data being transmitted (e.g., user details).
- Signature: A cryptographic hash generated using the payload and a secret key, ensuring the token’s authenticity.
By verifying the signature with the public key, Replyke can trust that the data hasn’t been tampered with and was signed by the correct private key.
Overview of the Process
Generating JWT Keys
- Developers generate a pair of JWT keys (public and private) from the Replyke Dashboard.
- The public key is stored with Replyke and associated with the project.
- The private key is shown only once during generation and must be securely stored by the developer, typically as an environment variable on their server.
Signing the JWT
- The private key is used to sign a JWT containing user details.
- This ensures the data’s integrity and authenticity, as the public key on Replyke’s side can verify it.
Obtaining User Data on the Server
- When a logged-in user is detected, the app makes a request to the developer’s server (not Replyke) to obtain a signed JWT.
- The server retrieves the user’s data (e.g., from cookies or authentication headers) and signs the JWT with the private key.
- This approach ensures that user data is never directly sent from the client to the server, reducing the risk of tampering.
Passing the JWT to the Client
- The signed JWT is sent back to the client and passed as a prop to the
AuthProvider
in Replyke.
Verification and User Association in Replyke
- Replyke verifies the JWT using the public key.
- If verification succeeds, Replyke creates a user in its system linked to the external system’s user ID. All actions performed by this user are now securely associated with them.
Benefits of this Approach
- Security: Ensures user data integrity and prevents unauthorized actions.
- Flexibility: Allows developers to use any user management system with Replyke.
- Seamless Integration: Replyke transparently handles user association and data linking without compromising security.
The following section provides a detailed, step-by-step guide for implementing this integration in your project.
Example Implementation
This guide explains how to integrate an external user system with Replyke. For demonstration purposes, we’ll use Clerk as the authentication provider. However, the logic can be adapted for other systems, with slight variations based on their specific requirements.
Client-Side Integration
Overview
In this example, we check for a logged-in user from the external authentication system (Clerk). If a user is logged in, a request is made to the backend to retrieve a signed token, which is then consumed by the Replyke AuthProvider
.
Implementation
Here’s the React client-side implementation:
import { useEffect, useState } from "react";
import { AuthProvider, ProjectProvider } from "replyke";
import axios from "axios";
import { useUser, useAuth } from "@clerk/clerk-react";
function ContextProvider({ children }: { children: React.ReactNode }) {
const { user: userClerk } = useUser();
const { getToken } = useAuth();
const [signedToken, setSignedToken] = useState<string | null>(null);
useEffect(() => {
const generateJwt = async () => {
try {
const token = await getToken(); // Get the Clerk session token
const path = "<YOUR_SERVER_ENDPOINT>"; // Replace with your server's endpoint
const response = await axios.get(path, {
headers: {
Authorization: `Bearer ${token}`, // Attach token to Authorization header
},
});
if (response) setSignedToken(response.data);
} catch (error) {
console.error("Error fetching data:", error);
throw error; // Re-throw the error for further handling
}
};
if (userClerk) generateJwt();
}, [userClerk, getToken]);
return (
<ProjectProvider projectId="<YOUR_PROJECT_ID>">
<AuthProvider signedToken={signedToken}>
{children}
</AuthProvider>
</ProjectProvider>
);
}
export default ContextProvider;
Key Points:
- Token Management: The Clerk session token is retrieved using
getToken
. - API Call: A
GET
request is sent to the server with the token included in theAuthorization
header. For other systems, ensure their tokens are passed as required. - State Management: The signed token received from the server is stored in the component state and consumed by the Replyke
AuthProvider
.
Server-Side Integration
Overview
On the server, we:
- Use Clerk middleware to handle authentication.
- Implement an endpoint to issue a signed token for Replyke.
Middleware Setup
To enable Clerk’s middleware:
// Apply Clerk middleware
app.use(clerkMiddleware());
This ensures requests are authenticated before reaching protected routes.
Endpoint Definition
We protect the route using Clerk’s requireAuth
middleware and delegate the logic to a controller:
import { Router } from "express";
import { requireAuth } from "@clerk/express";
import signUserJwt from "../controllers/auth/signUserJwt";
const router = Router();
router.get("/sign-token", requireAuth(), signUserJwt);
export default router;
Controller Logic
The controller generates a signed JWT token:
import { clerkClient } from "@clerk/express";
import { Request as ExReq, Response as ExRes } from "express";
import jwt from "jsonwebtoken";
export default async (req: ExReq, res: ExRes) => {
try {
const { userId } = req.auth;
const user = await clerkClient.users.getUser(userId);
const { emailAddresses } = user;
const email = emailAddresses.find(
(email) => email.id === user.primaryEmailAddressId
)?.emailAddress;
const projectId = process.env.REPLYKE_PROJECT_ID;
const privateKeyPem = Buffer.from(
process.env.REPLYKE_PROJECT_PRIVATE_KEY!,
"base64"
).toString("utf-8");
const payload = {
sub: userId, // External user's ID
iss: projectId, // Project ID
aud: "replyke.com", // Audience
userData: {
email,
},
};
const token = jwt.sign(payload, privateKeyPem, {
algorithm: "RS256",
expiresIn: "5m",
});
return res.status(200).send(token);
} catch (err: any) {
console.error("Error generating token: ", err);
return res.status(500).send({ error: "Server error" });
}
};
Key Points:
- Middleware: Clerk’s middleware attaches the user’s ID to the request object.
- Private Key: The private key is converted from a base64 string to a PEM format.
- JWT Payload:
sub
: User ID.iss
: Project ID.aud
: Always set toreplyke.com
.userData
: Optional user details. In this example we pass the email only, but we can pass all properties that are avaialable on the User object (e.g. name, username, location, metadata etc.)
- Token Signing: The JWT is signed using
RS256
with a short expiration time (5 minutes).
Customizing for Other Systems
To adapt this integration for other authentication systems:
- Replace Clerk-specific logic (e.g.,
getToken
,requireAuth
, and user details extraction) with equivalent methods from your chosen system. - Ensure your authentication middleware populates the request with user data.
- Follow Replyke’s JWT payload structure for compatibility.
Conclusion
This example demonstrates a secure and modular approach to integrating an external user system with Replyke. While we used Clerk for demonstration, the same principles apply to other systems with minor adjustments. This flexibility ensures seamless integration with your existing authentication setup.