Code-First Security: A Practical Implementation of a Go Passkey Manager
Introduction — What are ‘Passkeys’ and why should we use them?
I was asked by a young student asking me about passkeys, why we should use them, so I decided to implement a sample code to make a demonstration. This simple question highlights a major shift in digital security: we are finally moving away from the era of shared secrets. While passwords require us to memorize complex strings — which we inevitably reuse or forget — passkeys leverage asymmetric cryptography to create a login experience that is both more secure and significantly more convenient.
The core advantage of passkeys lies in their phishing resistance. Because a passkey is tied to a specific domain and never leaves your physical device, a malicious website cannot “trick” you into giving up your credentials. Furthermore, passkeys eliminate the risk of server-side data breaches; since the server only stores a public key rather than a password hash, there is no “secret” for a hacker to steal. By replacing manual entry with biometrics like FaceID or TouchID, passkeys offer a seamless “zero-friction” workflow that proves security doesn’t always have to come at the cost of user experience.
Why Passkeys Win: A Quick Comparison
| Feature | Traditional Passwords | Passkeys (WebAuthn) |
| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------ |
| **Authentication Type** | Knowledge (What you know) | Possession + Inherence (What you have + Who you are) |
| **Phishing Protection** | **Low:** Users can be tricked into typing passwords. | **High:** Cryptographically bound to the real website. |
| **Data Breach Risk** | **High:** Stolen hashes can be cracked offline. | **None:** Public keys are useless to an attacker. |
| **User Experience** | **Frustrating:** Requires managers or memory. | **Seamless:** Uses device biometrics or PINs. |
Sample Application to showcase a practical ‘Passkey’ implementation

To transform the abstract theory of FIDO2 into a tangible learning tool, I collaborated with IBM Bob to architect a functional passkey demonstration. The system utilizes a robust Go backend to manage the complex WebAuthn handshake, paired with a clean HTML/JavaScript frontend for a transparent user experience. During my testing, the application generated passkeys seamlessly, which I was able to store securely within my Bitwarden manager — proving that these ‘hardware-backed’ credentials integrate perfectly with modern security workflows. For those looking to dive deeper into the ‘how’ and ‘why,’ Bob has produced comprehensive documentation within the project, including a Quick Start Guide and detailed Architecture Diagrams, ensuring the mechanics are as clear as the implementation itself.
Project Breakdown at a Glance
Based on the project files, here is how the educational application is structured:
passkey-demo/
├── server/ # Go backend server
│ ├── main.go # Server entry point
│ ├── handlers.go # HTTP handlers
│ ├── storage.go # In-memory storage
│ ├── go.mod # Go dependencies
│ └── go.sum # Dependency checksums
├── client/ # HTML/JavaScript client
│ ├── index.html # Main page
│ ├── register.html # Registration page
│ └── login.html # Login page
├── k8s/ # Kubernetes manifests
│ ├── namespace.yaml # Namespace definition
│ ├── configmap.yaml # Configuration
│ ├── deployment.yaml # Deployment spec
│ ├── service.yaml # Service definition
│ ├── ingress.yaml # Ingress rules
│ ├── hpa.yaml # Horizontal Pod Autoscaler
│ └── README.md # K8s deployment guide
├── Dockerfile # Container image definition
├── .dockerignore # Docker ignore rules
├── docker-compose.yml # Docker Compose configuration
├── start.sh # Start script (detached mode)
├── stop.sh # Stop script
├── ARCHITECTURE.md # Architecture diagrams (Mermaid)
├── QUICKSTART.md # Quick start guide
├── SETUP.md # Detailed setup instructions
├── API.md # API documentation
└── PROJECT_OVERVIEW.md # Comprehensive overview
- The Backend (Go): A RESTful API that leverages the go-webauthn library to handle registration and login challenges.
package main
import (
"log"
"net/http"
"os"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/gorilla/mux"
)
func main() {
// Get configuration from environment or use defaults
rpID := getEnv("RP_ID", "localhost")
rpOrigin := getEnv("RP_ORIGIN", "http://localhost:8080")
port := getEnv("PORT", "8080")
// Initialize WebAuthn
wconfig := &webauthn.Config{
RPDisplayName: "Passkey Demo",
RPID: rpID,
RPOrigins: []string{rpOrigin},
// Timeout for registration/authentication (in milliseconds)
Timeouts: webauthn.TimeoutsConfig{
Login: webauthn.TimeoutConfig{
Enforce: true,
Timeout: 60000,
TimeoutUVD: 60000,
},
Registration: webauthn.TimeoutConfig{
Enforce: true,
Timeout: 60000,
TimeoutUVD: 60000,
},
},
}
webAuthn, err := webauthn.New(wconfig)
if err != nil {
log.Fatalf("Failed to create WebAuthn instance: %v", err)
}
// Initialize storage
storage := NewStorage()
// Initialize handlers
handlers := NewHandlers(webAuthn, storage)
// Setup router
r := mux.NewRouter()
// API routes
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/register/begin", handlers.RegisterBegin).Methods("POST", "OPTIONS")
api.HandleFunc("/register/finish", handlers.RegisterFinish).Methods("POST", "OPTIONS")
api.HandleFunc("/login/begin", handlers.LoginBegin).Methods("POST", "OPTIONS")
api.HandleFunc("/login/finish", handlers.LoginFinish).Methods("POST", "OPTIONS")
// Health check endpoint
r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}).Methods("GET")
// Serve static files from client directory
clientDir := "../client"
if _, err := os.Stat(clientDir); err == nil {
r.PathPrefix("/").Handler(http.FileServer(http.Dir(clientDir)))
log.Printf("Serving static files from %s", clientDir)
} else {
log.Printf("Client directory not found at %s, skipping static file serving", clientDir)
}
// Apply middleware
handler := LoggingMiddleware(EnableCORS(r))
// Start server
log.Printf("Starting server on port %s", port)
log.Printf("RP ID: %s", rpID)
log.Printf("RP Origin: %s", rpOrigin)
log.Printf("API endpoints:")
log.Printf(" POST /api/register/begin")
log.Printf(" POST /api/register/finish")
log.Printf(" POST /api/login/begin")
log.Printf(" POST /api/login/finish")
log.Printf(" GET /health")
if err := http.ListenAndServe(":"+port, handler); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
// getEnv gets an environment variable or returns a default value
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
// Made with Bob
-
The Frontend (Vanilla JS): Uses the native
navigator.credentialsbrowser API to talk to your biometrics or security keys.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Passkey Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 500px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 10px;
text-align: center;
}
.subtitle {
color: #666;
margin-bottom: 30px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 600;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
.btn {
width: 100%;
padding: 15px;
font-size: 1.1em;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
margin-top: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid #667eea;
}
.btn-secondary:hover {
background: #f5f5f5;
}
.message {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.back-link {
text-align: center;
margin-top: 20px;
}
.back-link a {
color: #667eea;
text-decoration: none;
}
.back-link a:hover {
text-decoration: underline;
}
.loading {
display: none;
text-align: center;
margin: 20px 0;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.info-box {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Register</h1>
<p class="subtitle">Create a new account with a passkey</p>
<div id="message" class="message"></div>
<div class="info-box">
<strong>How it works:</strong> Enter your username and click register. You'll be prompted to create a passkey using your device's biometric sensor, security key, or PIN.
</div>
<form id="registerForm">
<div class="form-group">
<label for="username">Username or Email</label>
<input
type="text"
id="username"
name="username"
placeholder="Enter your username or email"
required
autocomplete="username webauthn"
>
</div>
<button type="submit" class="btn btn-primary" id="registerBtn">
Register with Passkey
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p style="margin-top: 10px; color: #666;">Creating your passkey...</p>
</div>
<div class="back-link">
<a href="index.html">← Back to Home</a>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8080/api';
// Helper function to show messages
function showMessage(text, type) {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.className = `message ${type}`;
messageEl.style.display = 'block';
}
// Helper function to hide messages
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
// Helper function to show/hide loading
function setLoading(isLoading) {
document.getElementById('loading').style.display = isLoading ? 'block' : 'none';
document.getElementById('registerBtn').disabled = isLoading;
document.getElementById('registerForm').style.display = isLoading ? 'none' : 'block';
}
// Convert base64url to ArrayBuffer
function base64urlToBuffer(base64url) {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const padLen = (4 - (base64.length % 4)) % 4;
const padded = base64 + '='.repeat(padLen);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
// Convert ArrayBuffer to base64url
function bufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Handle registration form submission
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
hideMessage();
const username = document.getElementById('username').value.trim();
if (!username) {
showMessage('Please enter a username', 'error');
return;
}
try {
setLoading(true);
// Step 1: Begin registration
const beginResponse = await fetch(`${API_BASE}/register/begin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
if (!beginResponse.ok) {
const error = await beginResponse.text();
throw new Error(error || 'Failed to begin registration');
}
const options = await beginResponse.json();
console.log('Registration options:', options);
// Convert base64url strings to ArrayBuffers
options.publicKey.challenge = base64urlToBuffer(options.publicKey.challenge);
options.publicKey.user.id = base64urlToBuffer(options.publicKey.user.id);
if (options.publicKey.excludeCredentials) {
options.publicKey.excludeCredentials = options.publicKey.excludeCredentials.map(cred => ({
...cred,
id: base64urlToBuffer(cred.id)
}));
}
// Step 2: Create credential
showMessage('Please follow your device prompts to create a passkey...', 'info');
const credential = await navigator.credentials.create(options);
console.log('Credential created:', credential);
// Step 3: Finish registration
const credentialJSON = {
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
type: credential.type,
response: {
attestationObject: bufferToBase64url(credential.response.attestationObject),
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON)
}
};
const finishResponse = await fetch(`${API_BASE}/register/finish?username=${encodeURIComponent(username)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialJSON)
});
if (!finishResponse.ok) {
const error = await finishResponse.text();
throw new Error(error || 'Failed to complete registration');
}
const result = await finishResponse.json();
console.log('Registration result:', result);
showMessage('Registration successful! Redirecting to login...', 'success');
setTimeout(() => {
window.location.href = 'login.html';
}, 2000);
} catch (error) {
console.error('Registration error:', error);
showMessage(`Registration failed: ${error.message}`, 'error');
setLoading(false);
}
});
// Check WebAuthn support
if (!window.PublicKeyCredential) {
showMessage('WebAuthn is not supported in this browser. Please use a modern browser.', 'error');
document.getElementById('registerBtn').disabled = true;
}
</script>
</body>
</html>
- Storage Strategy: Currently uses in-memory storage for simplicity, making it a perfect “sandbox” for students to experiment without database overhead.
-
Deployment Versatility: Bob has configured the app to run via a simple script (
start.sh), Docker, or even Kubernetes for more advanced learners.
# Multi-stage build for smaller final image
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Set working directory
WORKDIR /build
# Copy go mod files
COPY server/go.mod server/go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY server/ ./
# Build the application
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o passkey-demo .
# Final stage - minimal image
FROM alpine:latest
# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates tzdata
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Set working directory
WORKDIR /app
# Copy binary from builder
COPY --from=builder /build/passkey-demo .
# Copy client files
COPY client/ ./client/
# Change ownership to non-root user
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Set environment variables
ENV RP_ID=localhost \
RP_ORIGIN=http://localhost:8080 \
PORT=8080
# Run the application
CMD ["./passkey-demo"]
apiVersion: apps/v1
kind: Deployment
metadata:
name: passkey-demo
namespace: passkey-demo
labels:
app: passkey-demo
spec:
replicas: 3
selector:
matchLabels:
app: passkey-demo
template:
metadata:
labels:
app: passkey-demo
spec:
containers:
- name: passkey-demo
image: passkey-demo:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: RP_ID
valueFrom:
configMapKeyRef:
name: passkey-demo-config
key: RP_ID
- name: RP_ORIGIN
valueFrom:
configMapKeyRef:
name: passkey-demo-config
key: RP_ORIGIN
- name: PORT
valueFrom:
configMapKeyRef:
name: passkey-demo-config
key: PORT
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
# Made with Bob
Understanding the “Bitwarden” Connection
When you store a passkey in Bitwarden during this demo, you are witnessing cross-platform passkey support. The Go server sends a challenge, and instead of your laptop’s local chip (TPM) answering, your Bitwarden browser extension intercepts the request, signs it, and saves the private key.
Conclusion
In conclusion, the fundamental principle of passkeys represents a paradigm shift from “something you know” to “something you have” and “something you are,” replacing vulnerable shared secrets with robust asymmetric cryptography. This demonstration application successfully showcases the efficiency of this transition by distilling a complex security protocol into a frictionless, one-touch experience. By removing the burden of password creation and management while simultaneously neutralizing the threat of phishing and server-side breaches, the project proves that high-level security does not have to compromise user convenience. As users seamlessly generate and store these credentials in tools like Bitwarden, it becomes clear that passkeys are not just a theoretical improvement, but a practical, ready-to-use standard for a more secure digital future.
>>> Thanks for reading 🔐 <<<
Links
- Repository of this demonstration: https://github.com/aairom/passkey-demo
- Web authentication explananation: https://webauthn.guide/
- Fido alliance: https://fidoalliance.org/
- Go Webauthnetication Library used: https://github.com/go-webauthn/webauthn






Top comments (0)