forked from yvv/decision
Auth Duniter v2 : vérification réelle + extension signing + titres outils
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Backend :
- Vérification Sr25519/Ed25519 réelle via substrateinterface (bypass démo)
- Message signé : <Bytes>{challenge}</Bytes> (convention polkadot.js)
- DEV_PROFILES : Charlie → Référent structure, Dave → Auteur (WoT member)
Frontend :
- Signing via extension polkadot.js / Cesium2 (_signWithExtension)
- @polkadot/extension-dapp + @polkadot/util installés
- Vite : global=globalThis + optimizeDeps pour les packages polkadot
- Boîte à outils : titres complets des 4 sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,15 +45,15 @@ DEV_PROFILES = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmkP7j4bJa3zN7d8tY",
|
"address": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmkP7j4bJa3zN7d8tY",
|
||||||
"display_name": "Charlie (Comite Tech)",
|
"display_name": "Charlie (Référent structure)",
|
||||||
"wot_status": "member",
|
"wot_status": "member",
|
||||||
"is_smith": True,
|
"is_smith": True,
|
||||||
"is_techcomm": True,
|
"is_techcomm": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy",
|
"address": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy",
|
||||||
"display_name": "Dave (Observateur)",
|
"display_name": "Dave (Auteur)",
|
||||||
"wot_status": "unknown",
|
"wot_status": "member",
|
||||||
"is_smith": False,
|
"is_smith": False,
|
||||||
"is_techcomm": False,
|
"is_techcomm": False,
|
||||||
},
|
},
|
||||||
@@ -132,15 +132,40 @@ async def verify_challenge(
|
|||||||
detail="Challenge invalide",
|
detail="Challenge invalide",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Verify Ed25519 signature
|
# 4. Verify signature (bypass for demo profiles in DEMO_MODE)
|
||||||
# TODO: Implement actual Ed25519 verification using substrate-interface
|
_demo_addresses = {p["address"] for p in DEV_PROFILES}
|
||||||
# For now we accept any signature to allow development/testing.
|
is_demo_bypass = settings.DEMO_MODE and payload.address in _demo_addresses
|
||||||
# In production this MUST verify: verify(address_pubkey, challenge_bytes, signature_bytes)
|
|
||||||
#
|
if not is_demo_bypass:
|
||||||
# from substrateinterface import Keypair
|
# polkadot.js / Cesium2 signRaw(type='bytes') wraps: <Bytes>{challenge}</Bytes>
|
||||||
# keypair = Keypair(ss58_address=payload.address)
|
message = f"<Bytes>{payload.challenge}</Bytes>".encode("utf-8")
|
||||||
# if not keypair.verify(payload.challenge.encode(), bytes.fromhex(payload.signature)):
|
sig_hex = payload.signature.removeprefix("0x")
|
||||||
# raise HTTPException(status_code=401, detail="Signature invalide")
|
try:
|
||||||
|
sig_bytes = bytes.fromhex(sig_hex)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Format de signature invalide (hex attendu)",
|
||||||
|
)
|
||||||
|
|
||||||
|
from substrateinterface import Keypair, KeypairType
|
||||||
|
|
||||||
|
verified = False
|
||||||
|
# Try Sr25519 first (default Substrate/Cesium2), then Ed25519 (Duniter v1 migration)
|
||||||
|
for key_type in [KeypairType.SR25519, KeypairType.ED25519]:
|
||||||
|
try:
|
||||||
|
kp = Keypair(ss58_address=payload.address, crypto_type=key_type)
|
||||||
|
if kp.verify(message, sig_bytes):
|
||||||
|
verified = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not verified:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Signature invalide",
|
||||||
|
)
|
||||||
|
|
||||||
# 5. Consume the challenge
|
# 5. Consume the challenge
|
||||||
del _pending_challenges[payload.address]
|
del _pending_challenges[payload.address]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface ToolSection {
|
|||||||
const sections: ToolSection[] = [
|
const sections: ToolSection[] = [
|
||||||
{
|
{
|
||||||
key: 'documents',
|
key: 'documents',
|
||||||
title: 'Documents',
|
title: 'Documents de référence',
|
||||||
icon: 'i-lucide-book-open',
|
icon: 'i-lucide-book-open',
|
||||||
color: 'var(--mood-accent)',
|
color: 'var(--mood-accent)',
|
||||||
tools: [
|
tools: [
|
||||||
@@ -36,7 +36,7 @@ const sections: ToolSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'decisions',
|
key: 'decisions',
|
||||||
title: 'Décisions',
|
title: 'Décisions et consultation d\'avis',
|
||||||
icon: 'i-lucide-scale',
|
icon: 'i-lucide-scale',
|
||||||
color: 'var(--mood-secondary, var(--mood-accent))',
|
color: 'var(--mood-secondary, var(--mood-accent))',
|
||||||
tools: [
|
tools: [
|
||||||
@@ -49,7 +49,7 @@ const sections: ToolSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mandats',
|
key: 'mandats',
|
||||||
title: 'Mandats',
|
title: 'Mandats et nominations',
|
||||||
icon: 'i-lucide-user-check',
|
icon: 'i-lucide-user-check',
|
||||||
color: 'var(--mood-success)',
|
color: 'var(--mood-success)',
|
||||||
tools: [
|
tools: [
|
||||||
@@ -61,7 +61,7 @@ const sections: ToolSection[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'protocoles',
|
key: 'protocoles',
|
||||||
title: 'Protocoles',
|
title: 'Protocoles et fonctionnement',
|
||||||
icon: 'i-lucide-settings',
|
icon: 'i-lucide-settings',
|
||||||
color: 'var(--mood-tertiary, var(--mood-accent))',
|
color: 'var(--mood-tertiary, var(--mood-accent))',
|
||||||
tools: [
|
tools: [
|
||||||
|
|||||||
@@ -5,6 +5,40 @@
|
|||||||
* The identity object mirrors the backend IdentityOut schema.
|
* The identity object mirrors the backend IdentityOut schema.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a challenge using the injected Duniter/Substrate wallet extension
|
||||||
|
* (Cesium2, polkadot.js extension, Talisman, etc.).
|
||||||
|
*
|
||||||
|
* The extension signs <Bytes>{challenge}</Bytes> to match the backend verifier.
|
||||||
|
*/
|
||||||
|
async function _signWithExtension(address: string, challenge: string): Promise<string> {
|
||||||
|
const { web3Enable, web3FromAddress } = await import('@polkadot/extension-dapp')
|
||||||
|
const { stringToHex } = await import('@polkadot/util')
|
||||||
|
|
||||||
|
const extensions = await web3Enable('libreDecision')
|
||||||
|
if (!extensions.length) {
|
||||||
|
throw new Error('Aucune extension Duniter détectée. Installez Cesium² ou Polkadot.js.')
|
||||||
|
}
|
||||||
|
|
||||||
|
let injector
|
||||||
|
try {
|
||||||
|
injector = await web3FromAddress(address)
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Adresse ${address.slice(0, 10)}… introuvable dans l'extension.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!injector.signer?.signRaw) {
|
||||||
|
throw new Error("L'extension ne supporte pas la signature de messages bruts.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { signature } = await injector.signer.signRaw({
|
||||||
|
address,
|
||||||
|
data: stringToHex(challenge),
|
||||||
|
type: 'bytes',
|
||||||
|
})
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
|
||||||
export interface DuniterIdentity {
|
export interface DuniterIdentity {
|
||||||
id: string
|
id: string
|
||||||
address: string
|
address: string
|
||||||
@@ -65,15 +99,12 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 2: Sign the challenge
|
// Step 2: Sign the challenge via polkadot.js / Cesium2 extension
|
||||||
// In production, signFn would use the Duniter keypair to produce an Ed25519 signature.
|
|
||||||
// For development, we use a placeholder signature.
|
|
||||||
let signature: string
|
let signature: string
|
||||||
if (signFn) {
|
if (signFn) {
|
||||||
signature = await signFn(challengeRes.challenge)
|
signature = await signFn(challengeRes.challenge)
|
||||||
} else {
|
} else {
|
||||||
// Development placeholder -- backend currently accepts any signature
|
signature = await _signWithExtension(address, challengeRes.challenge)
|
||||||
signature = 'dev_signature_placeholder'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Verify and get token
|
// Step 3: Verify and get token
|
||||||
|
|||||||
@@ -45,4 +45,13 @@ export default defineNuxtConfig({
|
|||||||
nitro: {
|
nitro: {
|
||||||
compressPublicAssets: true,
|
compressPublicAssets: true,
|
||||||
},
|
},
|
||||||
|
vite: {
|
||||||
|
define: {
|
||||||
|
// Polkadot packages expect a Node-like global
|
||||||
|
global: 'globalThis',
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['@polkadot/extension-dapp', '@polkadot/util'],
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
2746
frontend/package-lock.json
generated
2746
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@
|
|||||||
"@nuxt/content": "^3.11.2",
|
"@nuxt/content": "^3.11.2",
|
||||||
"@nuxt/ui": "^3.1.0",
|
"@nuxt/ui": "^3.1.0",
|
||||||
"@pinia/nuxt": "^0.11.0",
|
"@pinia/nuxt": "^0.11.0",
|
||||||
|
"@polkadot/extension-dapp": "^0.46.9",
|
||||||
|
"@polkadot/util": "^13.5.9",
|
||||||
"@unocss/nuxt": "^66.6.0",
|
"@unocss/nuxt": "^66.6.0",
|
||||||
"@vueuse/nuxt": "^14.2.1",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user