fix: add PKCE support for Logto auth and fix Traefik routing
Logto requires PKCE (Proof Key for Code Exchange) for SPA auth. Added code_challenge/code_verifier to login and callback flow. Also fixed Traefik router-service linking — when a container defines multiple routers, each needs an explicit service binding or Traefik v3 refuses to auto-link them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router';
|
||||
import { useAuthStore } from './auth-store';
|
||||
import { Spinner } from '@cameleer/design-system';
|
||||
import { fetchConfig } from '../config';
|
||||
import { getCodeVerifier } from './pkce';
|
||||
|
||||
export function CallbackPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -16,6 +17,12 @@ export function CallbackPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const codeVerifier = getCodeVerifier();
|
||||
if (!codeVerifier) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const redirectUri = `${window.location.origin}/callback`;
|
||||
|
||||
fetchConfig().then((config) => {
|
||||
@@ -27,6 +34,7 @@ export function CallbackPage() {
|
||||
code,
|
||||
client_id: config.logtoClientId,
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Spinner } from '@cameleer/design-system';
|
||||
import { fetchConfig } from '../config';
|
||||
import { generatePkce, storeCodeVerifier } from './pkce';
|
||||
|
||||
export function LoginPage() {
|
||||
const [config, setConfig] = useState<{ logtoEndpoint: string; logtoClientId: string } | null>(null);
|
||||
@@ -21,14 +22,18 @@ export function LoginPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
const handleLogin = async () => {
|
||||
if (!config?.logtoClientId) return;
|
||||
const { codeVerifier, codeChallenge } = await generatePkce();
|
||||
storeCodeVerifier(codeVerifier);
|
||||
const redirectUri = `${window.location.origin}/callback`;
|
||||
const params = new URLSearchParams({
|
||||
client_id: config.logtoClientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email offline_access',
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
});
|
||||
window.location.href = `${config.logtoEndpoint}/oidc/auth?${params}`;
|
||||
};
|
||||
|
||||
38
ui/src/auth/pkce.ts
Normal file
38
ui/src/auth/pkce.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
const VERIFIER_KEY = 'pkce_code_verifier';
|
||||
|
||||
function generateRandomString(length: number): string {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('').slice(0, length);
|
||||
}
|
||||
|
||||
async function sha256(plain: string): Promise<ArrayBuffer> {
|
||||
const encoder = new TextEncoder();
|
||||
return crypto.subtle.digest('SHA-256', encoder.encode(plain));
|
||||
}
|
||||
|
||||
function base64UrlEncode(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (const b of bytes) {
|
||||
binary += String.fromCharCode(b);
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
export async function generatePkce(): Promise<{ codeVerifier: string; codeChallenge: string }> {
|
||||
const codeVerifier = generateRandomString(64);
|
||||
const hashed = await sha256(codeVerifier);
|
||||
const codeChallenge = base64UrlEncode(hashed);
|
||||
return { codeVerifier, codeChallenge };
|
||||
}
|
||||
|
||||
export function storeCodeVerifier(verifier: string): void {
|
||||
sessionStorage.setItem(VERIFIER_KEY, verifier);
|
||||
}
|
||||
|
||||
export function getCodeVerifier(): string | null {
|
||||
const verifier = sessionStorage.getItem(VERIFIER_KEY);
|
||||
sessionStorage.removeItem(VERIFIER_KEY);
|
||||
return verifier;
|
||||
}
|
||||
Reference in New Issue
Block a user