gunauth
v0.1.0
Published
Minimal identity provider using GUN and SEA
Maintainers
Readme
GunAuth - Minimal Identity Provider
A minimal identity provider built with GUN and SEA, designed for peer-to-peer authentication with cross-domain session sharing capabilities.
✨ Features
- 🔐 Cryptographic Authentication using GUN's SEA (Security, Encryption, Authorization)
- 🔄 User Session Recall - Gun.js compatible
gun.user.recall()implementation - 🌐 Cross-Domain Sessions - Share authentication across multiple apps/domains
- 🔄 Two Session Sharing Methods:
- SSO Redirect Flow (OAuth2-like) - Most secure for production
- PostMessage API - Seamless UX for trusted domains
- 🚀 Zero-Config Setup - Works out of the box
- 📱 Framework Agnostic - Works with any frontend framework
- 🌊 P2P Ready - Built on GUN's distributed architecture
🎯 Cross-Domain Authentication
GunAuth now supports sharing authentication sessions across multiple applications and domains! Perfect for:
- Microservice architectures
- Multiple frontend applications
- Cross-domain SSO requirements
Quick Start: See the Cross-Domain Documentation for full implementation details.
📦 Installation
# Install
npm install gunauth
# Start the server
npm startOr use as a template:
npx create-gunauth-app my-auth-server🔄 User Session Recall
GunAuth now includes Gun.js compatible gun.user.recall() functionality for session restoration:
// Initialize GunAuth client
const gunauth = new GunAuthClient('http://localhost:8000');
// Login with recall data storage
await gunauth.loginWithRecall('username', 'password');
// Later, recall the user session
const recalled = await gunauth.user.recall();
if (recalled.success) {
console.log('User session restored:', recalled.username);
}
// Gun.js compatible API
gunauth.user.auth('username', 'password', callback);
gunauth.user.recall({password: 'password'}, callback);
console.log('Authenticated:', gunauth.user.is());See the User Recall Documentation for complete implementation details.
🧪 Examples & Testing
The examples/ directory contains complete working examples of cross-domain authentication:
# Quick demo setup
cd examples
./start-demo.sh
# Or test the API
npm run test:cross-domainExamples include:
- Two demo web applications showing session sharing
- Complete SSO and PostMessage client libraries
- Automated test suite
- Full documentation and setup guides
See the Examples README for details.
🚀 Deployment
This service is designed to run on any Node.js hosting platform. Below are detailed instructions for major cloud providers.
Deploy to Heroku
# Login to Heroku
heroku login
# Create a new Heroku app
heroku create your-app-name
# Deploy
git add .
git commit -m "Initial commit"
git push heroku mainDeploy to Vercel
Install Vercel CLI:
npm i -g vercelDeploy:
vercelCreate
vercel.jsonconfig:{ "version": 2, "builds": [ { "src": "index.js", "use": "@vercel/node" } ], "routes": [ { "src": "/(.*)", "dest": "index.js" } ] }
Deploy to Railway
# Install Railway CLI
npm install -g @railway/cli
# Login and deploy
railway login
railway init
railway upDeploy to AWS Lambda (Serverless)
Install Serverless Framework:
npm install -g serverless npm install serverless-httpCreate
serverless.yml:service: gunauth provider: name: aws runtime: nodejs18.x region: us-east-1 functions: app: handler: lambda.handler events: - http: path: /{proxy+} method: ANY cors: true - http: path: / method: ANY cors: trueCreate
lambda.jswrapper:import serverless from 'serverless-http'; import app from './index.js'; export const handler = serverless(app);Deploy:
serverless deploy
Deploy to Google Cloud Run
# Build and deploy
gcloud run deploy gunauth \
--source . \
--platform managed \
--region us-central1 \
--allow-unauthenticatedDeploy to Azure Container Apps
Create
Dockerfile:FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["npm", "start"]Deploy:
az containerapp up \ --name gunauth \ --source . \ --environment-variables PORT=3000
☁️ Using as Identity Provider with Major Clouds
GunAuth can serve as a custom identity provider for various cloud services and applications.
Integration with AWS
AWS API Gateway Custom Authorizer
// lambda-authorizer.js
export const handler = async (event) => {
const token = event.authorizationToken?.replace('Bearer ', '');
const pub = event.headers?.['X-Public-Key'];
try {
const response = await fetch(`${process.env.GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, pub })
});
const result = await response.json();
if (result.success) {
return {
principalId: result.claims.sub,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: 'Allow',
Resource: event.methodArn
}]
},
context: {
username: result.claims.sub,
issuer: result.claims.iss
}
};
}
} catch (error) {
console.error('Authorization failed:', error);
}
throw new Error('Unauthorized');
};AWS Cognito Custom Authentication Flow
// cognito-trigger.js
export const handler = async (event) => {
if (event.triggerSource === 'DefineAuthChallenge_Authentication') {
const { token, pub } = event.request.privateChallengeParameters;
const response = await fetch(`${process.env.GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, pub })
});
const result = await response.json();
event.response.answerCorrect = result.success;
}
return event;
};Integration with Google Cloud
Cloud Identity-Aware Proxy (IAP) Header Verification
// gcp-iap-middleware.js
import jwt from 'jsonwebtoken';
export const verifyGunAuthToken = async (req, res, next) => {
const token = req.headers['x-gunauth-token'];
const pub = req.headers['x-gunauth-pub'];
if (!token || !pub) {
return res.status(401).json({ error: 'Missing authentication headers' });
}
try {
const response = await fetch(`${process.env.GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, pub })
});
const result = await response.json();
if (result.success) {
req.user = result.claims;
next();
} else {
res.status(401).json({ error: 'Invalid token' });
}
} catch (error) {
res.status(500).json({ error: 'Authentication service error' });
}
};Google Cloud Functions Authentication
// functions/auth.js
import { onRequest } from 'firebase-functions/v2/https';
export const authenticatedFunction = onRequest(async (request, response) => {
const token = request.headers.authorization?.replace('Bearer ', '');
const pub = request.headers['x-public-key'];
if (!token || !pub) {
response.status(401).send('Unauthorized');
return;
}
try {
const authResponse = await fetch(`${process.env.GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, pub })
});
const result = await authResponse.json();
if (result.success) {
response.json({
message: 'Authenticated successfully',
user: result.claims.sub
});
} else {
response.status(401).send('Invalid token');
}
} catch (error) {
response.status(500).send('Authentication error');
}
});Integration with Microsoft Azure
Azure API Management Policy
<!-- Azure APIM Policy -->
<policies>
<inbound>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401">
<openid-config url="{{gunauth-url}}/.well-known/openid-configuration" />
<issuers>
<issuer>{{gunauth-url}}</issuer>
</issuers>
</validate-jwt>
<send-request mode="new" response-variable-name="authResponse">
<set-url>{{gunauth-url}}/verify</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>
@{
var token = context.Request.Headers["Authorization"].First().Replace("Bearer ", "");
var pub = context.Request.Headers["X-Public-Key"].First();
return JsonConvert.SerializeObject(new { token = token, pub = pub });
}
</set-body>
</send-request>
<choose>
<when condition="@(((IResponse)context.Variables["authResponse"]).StatusCode != 200)">
<return-response>
<set-status code="401" reason="Unauthorized" />
<set-body>Invalid authentication</set-body>
</return-response>
</when>
</choose>
</inbound>
</policies>Azure Functions with Custom Authentication
// Azure Function
import { app } from '@azure/functions';
app.http('authenticatedEndpoint', {
methods: ['GET', 'POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
const pub = request.headers.get('x-public-key');
if (!token || !pub) {
return { status: 401, body: 'Unauthorized' };
}
try {
const response = await fetch(`${process.env.GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, pub })
});
const result = await response.json();
if (result.success) {
return {
status: 200,
body: {
message: 'Success',
user: result.claims
}
};
} else {
return { status: 401, body: 'Invalid token' };
}
} catch (error) {
return { status: 500, body: 'Authentication error' };
}
}
});Integration with Kubernetes
Kubernetes Ingress with Authentication
# k8s-auth-middleware.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: gunauth-config
data:
GUNAUTH_URL: "https://your-gunauth-instance.com"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-middleware
spec:
replicas: 2
selector:
matchLabels:
app: auth-middleware
template:
metadata:
labels:
app: auth-middleware
spec:
containers:
- name: auth-middleware
image: your-registry/gunauth-middleware:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: gunauth-config
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: authenticated-ingress
annotations:
nginx.ingress.kubernetes.io/auth-url: "http://auth-middleware.default.svc.cluster.local:3000/verify"
nginx.ingress.kubernetes.io/auth-method: POST
nginx.ingress.kubernetes.io/auth-response-headers: X-User,X-Issuer
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: your-api-service
port:
number: 80Client Integration Examples
React/Next.js Frontend
// hooks/useGunAuth.js
import { useState, useEffect } from 'react';
const GUNAUTH_URL = process.env.NEXT_PUBLIC_GUNAUTH_URL;
export function useGunAuth() {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [pub, setPub] = useState(null);
const register = async (username, password) => {
const response = await fetch(`${GUNAUTH_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (result.success) {
setPub(result.pub);
return result;
}
throw new Error(result.error);
};
const login = async (username, password) => {
// Step 1: Request login challenge
const challengeResponse = await fetch(`${GUNAUTH_URL}/login-challenge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const challengeResult = await challengeResponse.json();
if (!challengeResult.success) {
throw new Error(challengeResult.error);
}
// Step 2: Get stored keypair and sign challenge
const keyPair = await getStoredKeyPair(password);
if (!keyPair || keyPair.pub !== challengeResult.pub) {
throw new Error('Invalid credentials or keypair not found');
}
const signature = await Gun.SEA.sign(challengeResult.challenge, keyPair);
// Step 3: Submit signature for verification
const verifyResponse = await fetch(`${GUNAUTH_URL}/login-verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
challenge: challengeResult.challenge,
signature
})
});
const result = await verifyResponse.json();
if (result.success) {
setToken(result.token);
setPub(result.pub);
localStorage.setItem('gunauth_token', result.token);
localStorage.setItem('gunauth_pub', result.pub);
return result;
}
throw new Error(result.error);
};
const logout = () => {
setToken(null);
setPub(null);
setUser(null);
localStorage.removeItem('gunauth_token');
localStorage.removeItem('gunauth_pub');
};
const verifyToken = async (tokenToVerify, pubKey) => {
const response = await fetch(`${GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: tokenToVerify, pub: pubKey })
});
const result = await response.json();
if (result.success) {
setUser(result.claims);
return result.claims;
}
return null;
};
// Auto-verify on mount
useEffect(() => {
const storedToken = localStorage.getItem('gunauth_token');
const storedPub = localStorage.getItem('gunauth_pub');
if (storedToken && storedPub) {
setToken(storedToken);
setPub(storedPub);
verifyToken(storedToken, storedPub);
}
}, []);
return {
user,
token,
pub,
register,
login,
logout,
verifyToken,
isAuthenticated: !!user
};
}Vue 3 Composition API
// composables/useGunAuth.js
import { ref, onMounted, computed } from 'vue'
const GUNAUTH_URL = import.meta.env.VITE_GUNAUTH_URL
export function useGunAuth() {
const user = ref(null)
const token = ref(null)
const pub = ref(null)
const loading = ref(false)
const error = ref(null)
const isAuthenticated = computed(() => !!user.value)
const register = async (username, password) => {
loading.value = true
error.value = null
try {
const response = await fetch(`${GUNAUTH_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
const result = await response.json()
if (result.success) {
pub.value = result.pub
return result
} else {
throw new Error(result.error)
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
const login = async (username, password) => {
loading.value = true
error.value = null
try {
// Step 1: Request login challenge
const challengeResponse = await fetch(`${GUNAUTH_URL}/login-challenge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
const challengeResult = await challengeResponse.json()
if (!challengeResult.success) {
throw new Error(challengeResult.error)
}
// Step 2: Get stored keypair and sign challenge
const keyPair = await getStoredKeyPair(password)
if (!keyPair || keyPair.pub !== challengeResult.pub) {
throw new Error('Invalid credentials or keypair not found')
}
const signature = await Gun.SEA.sign(challengeResult.challenge, keyPair)
// Step 3: Submit signature for verification
const verifyResponse = await fetch(`${GUNAUTH_URL}/login-verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
challenge: challengeResult.challenge,
signature
})
})
const result = await verifyResponse.json()
if (result.success) {
token.value = result.token
pub.value = result.pub
localStorage.setItem('gunauth_token', result.token)
localStorage.setItem('gunauth_pub', result.pub)
// Verify the token to get user claims
await verifyToken(result.token, result.pub)
return result
} else {
throw new Error(result.error)
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
const logout = () => {
token.value = null
pub.value = null
user.value = null
error.value = null
localStorage.removeItem('gunauth_token')
localStorage.removeItem('gunauth_pub')
}
const verifyToken = async (tokenToVerify, pubKey) => {
if (!tokenToVerify || !pubKey) return null
try {
const response = await fetch(`${GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: tokenToVerify, pub: pubKey })
})
const result = await response.json()
if (result.success) {
user.value = result.claims
return result.claims
} else {
// Token is invalid, clear stored data
logout()
return null
}
} catch (err) {
console.error('Token verification failed:', err)
logout()
return null
}
}
const refreshAuth = async () => {
const storedToken = localStorage.getItem('gunauth_token')
const storedPub = localStorage.getItem('gunauth_pub')
if (storedToken && storedPub) {
token.value = storedToken
pub.value = storedPub
await verifyToken(storedToken, storedPub)
}
}
// Auto-verify on mount
onMounted(async () => {
await refreshAuth()
})
return {
// State
user: readonly(user),
token: readonly(token),
pub: readonly(pub),
loading: readonly(loading),
error: readonly(error),
isAuthenticated,
// Methods
register,
login,
logout,
verifyToken,
refreshAuth
}
}Vue 3 Component Example
<!-- components/AuthForm.vue -->
<template>
<div class="auth-form">
<div v-if="!isAuthenticated">
<form @submit.prevent="handleSubmit">
<h2>{{ isLogin ? 'Login' : 'Register' }}</h2>
<div class="form-group">
<input
v-model="form.username"
type="text"
placeholder="Username"
required
:disabled="loading"
/>
</div>
<div class="form-group">
<input
v-model="form.password"
type="password"
placeholder="Password"
required
:disabled="loading"
/>
</div>
<div class="form-actions">
<button type="submit" :disabled="loading">
{{ loading ? 'Processing...' : (isLogin ? 'Login' : 'Register') }}
</button>
<button type="button" @click="toggleMode" :disabled="loading">
{{ isLogin ? 'Need to register?' : 'Already have account?' }}
</button>
</div>
<div v-if="error" class="error">
{{ error }}
</div>
</form>
</div>
<div v-else class="user-info">
<h2>Welcome, {{ user.sub }}!</h2>
<p>Logged in since: {{ new Date(user.iat).toLocaleString() }}</p>
<p>Session expires: {{ new Date(user.exp).toLocaleString() }}</p>
<button @click="logout" class="logout-btn">
Logout
</button>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useGunAuth } from '../composables/useGunAuth.js'
const { user, loading, error, isAuthenticated, register, login, logout } = useGunAuth()
const isLogin = ref(true)
const form = reactive({
username: '',
password: ''
})
const toggleMode = () => {
isLogin.value = !isLogin.value
form.username = ''
form.password = ''
}
const handleSubmit = async () => {
try {
if (isLogin.value) {
await login(form.username, form.password)
} else {
await register(form.username, form.password)
// Auto-login after successful registration
await login(form.username, form.password)
}
// Clear form on success
form.username = ''
form.password = ''
} catch (err) {
console.error('Authentication failed:', err)
}
}
</script>
<style scoped>
.auth-form {
max-width: 400px;
margin: 0 auto;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 1rem;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.form-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.form-actions button {
padding: 0.75rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.form-actions button[type="submit"] {
background-color: #007bff;
color: white;
}
.form-actions button[type="button"] {
background-color: #f8f9fa;
color: #6c757d;
}
.form-actions button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error {
color: #dc3545;
text-align: center;
font-size: 0.875rem;
}
.user-info {
text-align: center;
}
.logout-btn {
background-color: #dc3545;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.logout-btn:hover {
background-color: #c82333;
}
</style>Vue 3 Router Integration
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useGunAuth } from '../composables/useGunAuth.js'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { requiresGuest: true }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Navigation guard
router.beforeEach(async (to, from, next) => {
const { isAuthenticated, refreshAuth } = useGunAuth()
// Refresh auth state if needed
if (!isAuthenticated.value) {
await refreshAuth()
}
if (to.meta.requiresAuth && !isAuthenticated.value) {
next('/login')
} else if (to.meta.requiresGuest && isAuthenticated.value) {
next('/dashboard')
} else {
next()
}
})
export default routerVue 3 Pinia Store Integration
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
const GUNAUTH_URL = import.meta.env.VITE_GUNAUTH_URL
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(null)
const pub = ref(null)
const loading = ref(false)
const error = ref(null)
const isAuthenticated = computed(() => !!user.value)
const isTokenExpired = computed(() => {
if (!user.value?.exp) return true
return Date.now() > user.value.exp
})
const register = async (username, password) => {
loading.value = true
error.value = null
try {
const response = await fetch(`${GUNAUTH_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
const result = await response.json()
if (result.success) {
pub.value = result.pub
return result
} else {
throw new Error(result.error)
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
const login = async (username, password) => {
loading.value = true
error.value = null
try {
// Step 1: Request login challenge
const challengeResponse = await fetch(`${GUNAUTH_URL}/login-challenge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
const challengeResult = await challengeResponse.json()
if (!challengeResult.success) {
throw new Error(challengeResult.error)
}
// Step 2: Get stored keypair and sign challenge
const keyPair = await getStoredKeyPair(password)
if (!keyPair || keyPair.pub !== challengeResult.pub) {
throw new Error('Invalid credentials or keypair not found')
}
const signature = await Gun.SEA.sign(challengeResult.challenge, keyPair)
// Step 3: Submit signature for verification
const verifyResponse = await fetch(`${GUNAUTH_URL}/login-verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
challenge: challengeResult.challenge,
signature
})
})
const result = await verifyResponse.json()
if (result.success) {
token.value = result.token
pub.value = result.pub
localStorage.setItem('gunauth_token', result.token)
localStorage.setItem('gunauth_pub', result.pub)
// Verify token to get user claims
await verifyToken(result.token, result.pub)
return result
} else {
throw new Error(result.error)
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
const logout = () => {
token.value = null
pub.value = null
user.value = null
error.value = null
localStorage.removeItem('gunauth_token')
localStorage.removeItem('gunauth_pub')
}
const verifyToken = async (tokenToVerify, pubKey) => {
if (!tokenToVerify || !pubKey) return null
try {
const response = await fetch(`${GUNAUTH_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: tokenToVerify, pub: pubKey })
})
const result = await response.json()
if (result.success) {
user.value = result.claims
return result.claims
} else {
logout()
return null
}
} catch (err) {
console.error('Token verification failed:', err)
logout()
return null
}
}
const initAuth = async () => {
const storedToken = localStorage.getItem('gunauth_token')
const storedPub = localStorage.getItem('gunauth_pub')
if (storedToken && storedPub) {
token.value = storedToken
pub.value = storedPub
await verifyToken(storedToken, storedPub)
}
}
return {
// State
user,
token,
pub,
loading,
error,
isAuthenticated,
isTokenExpired,
// Actions
register,
login,
logout,
verifyToken,
initAuth
}
})Environment Variables for Cloud Integration
# Common environment variables for cloud deployments
GUNAUTH_URL=https://your-gunauth-instance.com
NODE_ENV=production
PORT=3000
# GUN relay configuration
GUN_RELAYS=https://your-relay1.com/gun,https://your-relay2.com/gun
# AWS specific
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# Google Cloud specific
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
# Azure specific
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_TENANT_ID=your-tenant-id📡 API Endpoints
POST /register
Register a new user and generate SEA key pair.
Request:
{
"username": "alice",
"password": "secure-password"
}Response:
{
"success": true,
"username": "alice",
"pub": "user-public-key",
"createdAt": 1642694400000
}POST /login-challenge
Request a cryptographic challenge for login (Gun SEA pattern).
Request:
{
"username": "alice",
"password": "secure-password"
}Response:
{
"success": true,
"challenge": "random-challenge-string",
"pub": "user-public-key"
}POST /login-verify
Verify signature and return signed token (Gun SEA pattern).
Request:
{
"username": "alice",
"challenge": "random-challenge-string",
"signature": "sea-signed-challenge"
}Response:
{
"success": true,
"token": "signed-jwt-token",
"pub": "user-public-key",
"exp": 1642698000000
}POST /verify
Verify a signed token.
Request:
{
"token": "signed-jwt-token",
"pub": "user-public-key"
}Response:
{
"success": true,
"claims": {
"sub": "alice",
"iss": "https://your-app-domain.com",
"iat": 1642694400000,
"exp": 1642698000000
},
"valid": true
}GET /user/:username/pub
Get user's public key by username.
Response:
{
"username": "alice",
"pub": "user-public-key",
"createdAt": 1642694400000
}🔗 GUN Relay Configuration
GunAuth uses multiple GUN relay peers for improved reliability and performance.
Default Relays
The application includes several reliable public GUN relays:
https://gun-manhattan.herokuapp.com/gun- Primary US relayhttps://gunjs.herokuapp.com/gun- Secondary US relayhttps://gun-us.herokuapp.com/gun- US East coast relayhttps://gun-eu.herokuapp.com/gun- European relayhttps://peer.wallie.io/gun- Community relayhttps://relay.peer.ooo/gun- Peer.ooo relay
Custom Relay Configuration
For production deployments, consider using your own GUN relays:
# Set custom relays via environment variable
export GUN_RELAYS="https://your-relay1.com/gun,https://your-relay2.com/gun,wss://your-relay1.com/gun"Hosting Your Own GUN Relay
// gun-relay-server.js
import Gun from 'gun';
import express from 'express';
import { createServer } from 'http';
const app = express();
const server = createServer(app);
// Serve GUN
app.use(Gun.serve);
server.listen(8765);
// Initialize Gun with persistence
const gun = Gun({
web: server,
file: 'data.json' // Local file storage
});
console.log('GUN relay server running on port 8765');Production Relay Best Practices
Multiple Relays: Use 3-5 relays for redundancy
Geographic Distribution: Spread relays across regions
HTTPS/WSS: Always use secure connections in production
Monitoring: Monitor relay health and connectivity
Backup: Regular backups of relay data
HTTPS/WSS: Always use secure connections in production
Monitoring: Monitor relay health and connectivity
Backup: Regular backups of relay data
🛡️ Production Considerations for Cloud Deployment
�️ Production Considerations for Cloud Deployment
Security Best Practices
- HTTPS Only: Always use HTTPS in production
- Rate Limiting: Implement rate limiting for authentication endpoints
- Input Validation: Validate all inputs server-side
- Environment Variables: Store sensitive config in environment variables
- Logging: Implement comprehensive logging for security monitoring
Monitoring & Observability
AWS CloudWatch Integration
// aws-cloudwatch-logger.js
import AWS from 'aws-sdk';
const cloudwatchlogs = new AWS.CloudWatchLogs();
export const logAuthEvent = async (event, username, success) => {
const params = {
logGroupName: '/aws/lambda/gunauth',
logStreamName: new Date().toISOString().split('T')[0],
logEvents: [{
timestamp: Date.now(),
message: JSON.stringify({
event,
username,
success,
timestamp: new Date().toISOString()
})
}]
};
try {
await cloudwatchlogs.putLogEvents(params).promise();
} catch (error) {
console.error('CloudWatch logging failed:', error);
}
};Google Cloud Logging
// gcp-logging.js
import { Logging } from '@google-cloud/logging';
const logging = new Logging();
const log = logging.log('gunauth');
export const logAuthEvent = async (event, username, success) => {
const metadata = {
resource: { type: 'global' },
severity: success ? 'INFO' : 'WARNING'
};
const entry = log.entry(metadata, {
event,
username,
success,
timestamp: new Date().toISOString()
});
await log.write(entry);
};Azure Application Insights
// azure-insights.js
import appInsights from 'applicationinsights';
appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING);
appInsights.start();
const client = appInsights.defaultClient;
export const logAuthEvent = (event, username, success) => {
client.trackEvent({
name: 'AuthenticationEvent',
properties: {
event,
username,
success: success.toString(),
timestamp: new Date().toISOString()
}
});
if (!success) {
client.trackException({
exception: new Error(`Authentication failed for ${username}`)
});
}
};Scaling Considerations
Load Balancing
- Use cloud load balancers (ALB, Cloud Load Balancing, Azure Load Balancer)
- Implement health checks on the
/endpoint - Consider sticky sessions if needed for GUN synchronization
Database Scaling
- GUN automatically handles peer-to-peer synchronization
- Consider deploying multiple GUN relay peers for redundancy
- Monitor GUN peer connectivity and sync status
Caching Strategy
// redis-cache-middleware.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export const cacheMiddleware = (ttl = 300) => {
return async (req, res, next) => {
if (req.method !== 'GET') return next();
const key = `cache:${req.originalUrl}`;
const cached = await redis.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
const originalSend = res.json;
res.json = function(data) {
redis.setex(key, ttl, JSON.stringify(data));
return originalSend.call(this, data);
};
next();
};
};High Availability Setup
Multi-Region Deployment
# docker-compose.ha.yml
version: '3.8'
services:
gunauth-primary:
image: gunauth:latest
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- GUN_PEERS=ws://gunauth-secondary:8765,ws://gunauth-tertiary:8765
ports:
- "3000:3000"
gunauth-secondary:
image: gunauth:latest
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- GUN_PEERS=ws://gunauth-primary:8765,ws://gunauth-tertiary:8765
ports:
- "3001:3000"
gunauth-tertiary:
image: gunauth:latest
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- GUN_PEERS=ws://gunauth-primary:8765,ws://gunauth-secondary:8765
ports:
- "3002:3000"
redis:
image: redis:alpine
ports:
- "6379:6379"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf🔐 Security Features
- Passwords are hashed using
SEA.work()before storage - Private keys are stored separately from user data
- Tokens expire after 1 hour
- Only public data is stored in GUN
- CORS enabled for browser requests
- Multiple relay peers for distributed resilience
- Dynamic issuer URL prevents token reuse across domains
🛠 Development
# Clone the repository
git clone https://github.com/draeder/gunauth.git
cd gunauth
# Install dependencies
npm install
# Start development server
npm run dev
# Start production server
npm start
# Run test
npm test📦 Tech Stack
- Node.js (ESM modules)
- Express.js (web server)
- GUN (distributed database)
- SEA (cryptographic functions)
🌐 Environment Variables
PORT- Server port (defaults to 3000)NODE_ENV- Environment modeISSUER_URL- JWT issuer URL (auto-detected from request if not set)GUN_RELAYS- Comma-separated list of GUN relay URLs (uses default relays if not set)
⚡ Usage Example
// Register a new user
const registerResponse = await fetch('https://your-app-domain.com/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'alice',
password: 'secure-password'
})
});
// Login using Gun SEA challenge-response pattern
const challengeResponse = await fetch('https://your-app-domain.com/login-challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'alice',
password: 'secure-password'
})
});
const challengeResult = await challengeResponse.json();
// Sign the challenge with stored keypair
const keyPair = await getStoredKeyPair('secure-password');
const signature = await Gun.SEA.sign(challengeResult.challenge, keyPair);
// Verify signature and get token
const loginResponse = await fetch('https://your-app-domain.com/login-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'alice',
challenge: challengeResult.challenge,
signature
})
});
const { token, pub } = await loginResponse.json();
// Verify token
const verifyResponse = await fetch('https://your-app-domain.com/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, pub })
});📄 License
MIT
A decentralized Identity Provider built on GUN SEA
