GitHub login in this project means: GitHub OAuth for identity + P256 passkey as AIN blockchain wallet. The user never manages private keys — the passkey IS the wallet.
- 1. Login Flow
- 2. Setup: GitHub OAuth App
- 3. How It Works (P256 Passkey)
- 4. Frontend Integration
- 5. KG Extractor Integration
- 6. Kubernetes CI/CD
User clicks "Sign in with GitHub"
→ GitHub OAuth (NextAuth) authenticates identity
→ After login, user registers a P256 passkey (WebAuthn)
- Browser prompts biometric / device PIN
- Creates ES256 (P256) keypair in OS keychain (Google Password Manager / iCloud Keychain)
→ P256 public key → keccak-256 hash → AIN blockchain address
→ Passkey IS the wallet. No seed phrase, no private key file.
For blockchain transactions:
→ navigator.credentials.get() signs a challenge with the passkey
→ P256 signature formatted for AIN blockchain
→ Transaction submitted on-chain
The two-step flow:
- GitHub OAuth — proves "who you are" (username, email, avatar)
- Passkey registration — creates "your wallet" (P256 keypair → AIN address)
- Go to https://github.com/settings/developers
- Click "New OAuth App"
- Fill in:
| Field | Value |
|---|---|
| Application name | Papers with Claude Code |
| Homepage URL | http://localhost:3000 (dev) or https://paperswithclaudecode.com |
| Authorization callback URL | http://localhost:3000/api/auth/callback/github |
- Click Register application
- Copy Client ID and generate a Client Secret
# GitHub OAuth
GITHUB_ID=your_client_id
GITHUB_SECRET=your_client_secret
# NextAuth
AUTH_SECRET=generate_a_random_secret
# AIN Blockchain
AIN_PROVIDER_URL=http://localhost:8081
AIN_PRIVATE_KEY=server_wallet_private_keyGenerate NEXTAUTH_SECRET:
openssl rand -base64 32The AIN blockchain was extended to support P256 (in addition to secp256k1):
Signature format:
secp256k1: 0x{hash(32)}{r(32)}{s(32)}{v(1)} — 66 bytes (existing)
P256: 0x02{compressedPubKey(33)}{r(32)}{s(32)} — 98 bytes (new)
- Prefix
0x02identifies P256 signatures - Compressed public key is included (P256 recovery is unreliable)
- Backward compatible — existing secp256k1 signatures unchanged
| File | Purpose |
|---|---|
ainblockchain-integration/debug-frontend/src/lib/passkey.ts |
WebAuthn helpers: registration, assertion, CBOR decode |
ainblockchain-integration/debug-frontend/src/components/AuthGate.tsx |
GitHub login + passkey registration UI |
ainblockchain-integration/debug-frontend/src/components/WalletInfo.tsx |
Passkey-derived AIN address display |
ainblockchain-integration/debug-frontend/src/lib/types.ts |
PasskeyCredential type |
ainblockchain-integration/p256-passkey-plan.md |
Full P256 implementation plan |
import {
getRegistrationOptions,
extractPublicKeyFromAttestation,
bytesToHex,
} from '@/lib/passkey';
// 1. Create WebAuthn credential with P256/ES256
const options = getRegistrationOptions(userId, userName);
const credential = await navigator.credentials.create(options);
// 2. Extract P256 public key from attestation
const response = credential.response as AuthenticatorAttestationResponse;
const pubKey = extractPublicKeyFromAttestation(response.attestationObject);
// pubKey = 65 bytes: 04 || x(32) || y(32)
// 3. Derive AIN address from P256 public key
const hashBuffer = await crypto.subtle.digest('SHA-256', pubKey);
const ainAddress = '0x' + bytesToHex(new Uint8Array(hashBuffer)).slice(-40);
// 4. Store credential locally
localStorage.setItem('passkey_info', JSON.stringify({
credentialId: bufferToBase64Url(credential.rawId),
publicKey: bytesToHex(pubKey),
ainAddress,
}));import { getAssertionOptions } from '@/lib/passkey';
// 1. Create challenge (usually the transaction hash)
const challenge = new Uint8Array(txHash);
// 2. Sign with passkey
const credentialId = JSON.parse(localStorage.getItem('passkey_info')).credentialId;
const assertion = await navigator.credentials.get(
getAssertionOptions(challenge, credentialId)
);
// 3. Extract R, S from the DER-encoded signature
const signature = assertion.response.signature;
// Parse DER → extract r(32), s(32)
// 4. Format as AIN P256 signature: 0x02 + compressedPubKey(33) + r(32) + s(32)The main frontend (frontend/ — live at paperswithclaudecode.com) implements the passkey auth flow.
1. Copy passkey library:
debug-frontend/src/lib/passkey.ts → frontend/src/lib/passkey.ts
debug-frontend/src/lib/types.ts → frontend/src/lib/types.ts (merge PasskeyCredential)
2. Replace or extend AuthProvider:
Currently the main frontend uses NextAuth v5 with JWT sessions. The passkey flow adds a second step after GitHub login:
// frontend/src/providers/AuthProvider.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export function AuthProvider({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<AuthSyncEffect />
<PasskeyEffect /> {/* NEW: auto-check passkey after login */}
{children}
</SessionProvider>
);
}3. Add passkey state to auth store:
// frontend/src/stores/useAuthStore.ts
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
// NEW: passkey/wallet fields
passkeyCredentialId: string | null;
ainAddress: string | null;
hasPasskey: boolean;
// ...
}4. Add passkey registration to login flow:
After GitHub OAuth succeeds, prompt passkey registration:
// frontend/src/app/login/page.tsx
// Step 1: GitHub OAuth
signIn('github', { redirectTo: '/setup-passkey' });
// Step 2: /setup-passkey page
// - Check if passkey exists in localStorage
// - If not, prompt "Register Passkey" button
// - On success, redirect to /explore5. Use passkey-derived address for blockchain calls:
// Instead of reading from blockchain/.env private key:
const ainAddress = useAuthStore(s => s.ainAddress);
// Use ainAddress for all ain.knowledge.* callsGitHub OAuth → session.user.username ("octocat")
session.user.id ("12345")
session.user.image (avatar URL)
Passkey Registration → passkeyInfo.ainAddress ("0xABC...")
passkeyInfo.credentialId (base64url)
passkeyInfo.publicKey (hex)
On-Chain Identity → /apps/knowledge/users/{ainAddress}/github_username = "octocat"
The KG extractor (knowledge-graph-builder/) generates course repos. It can use the GitHub identity in two ways:
When a logged-in user triggers course generation from the frontend, pass their GitHub + passkey identity:
# kg_extractor/scaffold.py — update profile generation
profile = {
"name": github_username, # from GitHub OAuth
"avatar": github_avatar_url, # GitHub avatar
"started_at": date.today().isoformat(),
"github_username": github_username, # GitHub login handle
"github_id": github_id, # GitHub user ID
"wallet_address": ain_address, # from passkey registration
}Add an HTTP endpoint so the frontend can trigger course generation with the authenticated user's context:
# POST /api/generate-course
# Headers: Authorization (validated by frontend session)
# Body:
{
"repo_url": "https://github.com/org/repo",
"github_username": "octocat", # from session
"github_id": "12345", # from session
"ain_address": "0xABC...", # from passkey
"enable_blockchain": true
}The generated course's .learner/profile.json will include the GitHub + wallet mapping.
For cloning private repos during course generation, set GITHUB_TOKEN:
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxGit automatically uses this for HTTPS clones. Alternatively, the frontend can forward the user's OAuth access token.
Store auth credentials as Kubernetes secrets:
# claudecode-kubernetes/base/secrets.yml
apiVersion: v1
kind: Secret
metadata:
name: github-oauth
namespace: papers-lms
type: Opaque
stringData:
GITHUB_ID: "<client-id>"
GITHUB_SECRET: "<client-secret>"
NEXTAUTH_SECRET: "<random-secret>"
---
apiVersion: v1
kind: Secret
metadata:
name: ain-blockchain
namespace: papers-lms
type: Opaque
stringData:
AIN_PROVIDER_URL: "http://ain-node:8081"
AIN_PRIVATE_KEY: "<server-wallet-key>"The frontend pod needs GitHub OAuth env vars for NextAuth and must serve on HTTPS (passkeys require a secure context):
# claudecode-kubernetes/frontend/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: papers-lms
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: ghcr.io/your-org/papers-lms/frontend:latest
ports:
- containerPort: 3000
env:
- name: GITHUB_ID
valueFrom:
secretKeyRef:
name: github-oauth
key: GITHUB_ID
- name: GITHUB_SECRET
valueFrom:
secretKeyRef:
name: github-oauth
key: GITHUB_SECRET
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: github-oauth
key: NEXTAUTH_SECRET
- name: NEXTAUTH_URL
value: "https://your-domain.com"
- name: AIN_PROVIDER_URL
valueFrom:
secretKeyRef:
name: ain-blockchain
key: AIN_PROVIDER_URLImportant: WebAuthn/passkeys require HTTPS. The ingress must terminate TLS:
# claudecode-kubernetes/base/ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: papers-lms
namespace: papers-lms
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- your-domain.com
secretName: papers-lms-tls
rules:
- host: your-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80.github/workflows/deploy.yml:
name: Build & Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository }}
jobs:
frontend:
runs-on: ubuntu-latest
if: contains(github.event.head_commit.modified, 'frontend/')
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push frontend
uses: docker/build-push-action@v5
with:
context: frontend
push: true
tags: ${{ env.IMAGE_PREFIX }}/frontend:${{ github.sha }}
- name: Deploy to K8s
uses: azure/setup-kubectl@v3
- run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config
kubectl set image deployment/frontend \
frontend=${{ env.IMAGE_PREFIX }}/frontend:${{ github.sha }} \
-n papers-lms
kg-extractor:
runs-on: ubuntu-latest
if: contains(github.event.head_commit.modified, 'knowledge-graph-builder/')
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push kg-extractor
uses: docker/build-push-action@v5
with:
context: knowledge-graph-builder
push: true
tags: ${{ env.IMAGE_PREFIX }}/kg-extractor:${{ github.sha }}
- name: Deploy to K8s
uses: azure/setup-kubectl@v3
- run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config
kubectl set image deployment/kg-extractor \
kg-extractor=${{ env.IMAGE_PREFIX }}/kg-extractor:${{ github.sha }} \
-n papers-lmsAdd these in Settings > Secrets and variables > Actions:
| Secret | Description |
|---|---|
KUBE_CONFIG |
Base64-encoded kubeconfig for cluster access |
GITHUB_TOKENis provided automatically by GitHub Actions. OAuth secrets (GITHUB_ID,GITHUB_SECRET) live in K8s secrets, not in GitHub Actions.
- HTTPS required: WebAuthn only works on
localhostor HTTPS origins - RP ID: The passkey's relying party ID (
rp.id) must match the production domain. Updatepasskey.ts:rp: { name: 'Papers with Claude Code', id: window.location.hostname, // automatically uses production domain }
- Cross-device: Passkeys created on one device can be used on another via QR code (platform-dependent)
- Passkey storage: Credentials are stored in
localStorageon the client. For cross-browser persistence, consider storing thecredentialId → ainAddressmapping server-side (keyed by GitHub user ID)
| Layer | Identifier | Source |
|---|---|---|
| GitHub | username (e.g. octocat) |
GitHub OAuth |
| Session | session.user.id + .name + .image |
NextAuth JWT |
| Wallet | ainAddress (e.g. 0xABC...) |
P256 passkey (WebAuthn) |
| On-chain | wallet_address |
AIN blockchain |
| Course | .learner/profile.json |
Scaffold + setup |
The passkey bridges GitHub identity to on-chain identity. No private keys are ever exposed to the user.