v1.7.11: Switch to Authorization Code + PKCE (OAuth 2.1 standard)
Reverts implicit flow workaround now that IAM fork properly reads OAuth params from the JSON body (hanzoai/iam@951e225a). All platforms now use response_type=code with PKCE: - Browser (Chrome/Firefox/Safari): code_challenge in authorize URL - VS Code: code exchange with code_verifier in callback server - CLI tools: same PKCE flow Still handles implicit flow as fallback if code not present.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hanzo-ai-monorepo",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"private": true,
|
||||
"description": "Hanzo AI Development Platform Monorepo",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/browser-extension",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"description": "Hanzo AI Browser Extension",
|
||||
"main": "dist/background.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -136,14 +136,13 @@ export async function login(): Promise<UserInfo> {
|
||||
const state = generateState();
|
||||
const redirectUri = getRedirectUri();
|
||||
|
||||
// Use response_type=token (implicit flow) — Casdoor returns the JWT
|
||||
// directly in the redirect URL. The code flow (response_type=code) requires
|
||||
// an empty-string grant_type that Casdoor's grant check doesn't support.
|
||||
// We still generate PKCE params in case code flow is ever re-enabled.
|
||||
// Authorization Code + PKCE (OAuth 2.1 standard for public clients)
|
||||
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
|
||||
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
|
||||
authorizeUrl.searchParams.set('response_type', 'token');
|
||||
authorizeUrl.searchParams.set('response_type', 'code');
|
||||
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
||||
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
||||
authorizeUrl.searchParams.set('scope', SCOPES);
|
||||
authorizeUrl.searchParams.set('state', state);
|
||||
|
||||
|
||||
@@ -529,8 +529,10 @@ async function firefoxLogin(): Promise<any> {
|
||||
|
||||
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
|
||||
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
|
||||
authorizeUrl.searchParams.set('response_type', 'token');
|
||||
authorizeUrl.searchParams.set('response_type', 'code');
|
||||
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
||||
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
||||
authorizeUrl.searchParams.set('scope', SCOPES);
|
||||
authorizeUrl.searchParams.set('state', state);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hanzo AI",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hanzo AI",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/dxt",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"description": "Hanzo AI DXT Bundle",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -4,7 +4,7 @@ pluginGroup = ai.hanzo
|
||||
pluginName = Hanzo AI
|
||||
pluginRepositoryUrl = https://github.com/hanzoai/hanzo-ai-jetbrains
|
||||
# SemVer format -> https://semver.org
|
||||
pluginVersion = 1.7.10
|
||||
pluginVersion = 1.7.11
|
||||
|
||||
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
||||
pluginSinceBuild = 233
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/cli-tools",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"description": "Hanzo AI CLI Tools Integration",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -172,15 +172,15 @@ export class HanzoAuth extends EventEmitter {
|
||||
});
|
||||
|
||||
this.server.listen(port, () => {
|
||||
// Build authorization URL
|
||||
// Use response_type=token (implicit flow) — Casdoor returns the JWT
|
||||
// directly in the redirect URL, avoiding the empty grant_type issue.
|
||||
// Authorization Code + PKCE (OAuth 2.1 standard)
|
||||
const authUrl = new URL('/login/oauth/authorize', this.config.iamLoginUrl);
|
||||
authUrl.searchParams.set('client_id', this.config.clientId);
|
||||
authUrl.searchParams.set('response_type', 'token');
|
||||
authUrl.searchParams.set('response_type', 'code');
|
||||
authUrl.searchParams.set('redirect_uri', `http://localhost:${port}/callback`);
|
||||
authUrl.searchParams.set('scope', this.config.scope);
|
||||
authUrl.searchParams.set('state', state);
|
||||
authUrl.searchParams.set('code_challenge', codeChallenge);
|
||||
authUrl.searchParams.set('code_challenge_method', 'S256');
|
||||
|
||||
// Open browser
|
||||
this.openBrowser(authUrl.toString());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@hanzo/extension",
|
||||
"displayName": "Hanzo AI",
|
||||
"description": "The ultimate meta AI development platform. Manage and run all LLMs and CLI tools (Claude, Codex, Gemini, OpenHands, Aider) in one unified interface. Features MCP integration, async execution, git worktrees, and universal context sync.",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"publisher": "hanzo-ai",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -162,19 +162,22 @@ export class StandaloneAuthManager {
|
||||
const deviceId = this.getDeviceId();
|
||||
const state = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Build OAuth URL — login UI is on hanzo.id, not iam.hanzo.ai
|
||||
// Use response_type=token (implicit flow) — Casdoor returns the JWT
|
||||
// directly in the redirect URL, avoiding the empty grant_type issue.
|
||||
// Build OAuth URL — Authorization Code + PKCE (OAuth 2.1 standard)
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
||||
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
||||
|
||||
const authUrl = new URL(`${this.casdoorConfig.loginUrl}/login/oauth/authorize`);
|
||||
authUrl.searchParams.append('client_id', this.casdoorConfig.clientId);
|
||||
authUrl.searchParams.append('response_type', 'token');
|
||||
authUrl.searchParams.append('response_type', 'code');
|
||||
authUrl.searchParams.append('redirect_uri', 'http://localhost:8765/callback');
|
||||
authUrl.searchParams.append('scope', 'read write');
|
||||
authUrl.searchParams.append('state', state);
|
||||
authUrl.searchParams.append('code_challenge', codeChallenge);
|
||||
authUrl.searchParams.append('code_challenge_method', 'S256');
|
||||
authUrl.searchParams.append('device_id', deviceId);
|
||||
|
||||
// Start local callback server
|
||||
const callbackResult = await this.startCallbackServer(state);
|
||||
const callbackResult = await this.startCallbackServer(state, codeVerifier);
|
||||
|
||||
// Open browser
|
||||
console.log('Opening browser for authentication...');
|
||||
@@ -204,66 +207,85 @@ export class StandaloneAuthManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
private async startCallbackServer(expectedState: string): Promise<{ accessToken: string; refreshToken?: string; expiresAt?: number } | null> {
|
||||
private async startCallbackServer(expectedState: string, codeVerifier?: string): Promise<{ accessToken: string; refreshToken?: string; expiresAt?: number } | null> {
|
||||
const endpoint = this.casdoorConfig.endpoint;
|
||||
const clientId = this.casdoorConfig.clientId;
|
||||
const clientSecret = this.casdoorConfig.clientSecret;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
|
||||
const server = http.createServer((req: any, res: any) => {
|
||||
const successHtml = `<html><body style="font-family:system-ui;text-align:center;padding:50px">
|
||||
<h1 style="color:#10b981">Authentication Successful!</h1>
|
||||
<p>You can close this window.</p>
|
||||
<script>setTimeout(()=>window.close(),3000)</script></body></html>`;
|
||||
|
||||
const server = http.createServer(async (req: any, res: any) => {
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
if (parsedUrl.pathname !== '/callback') return;
|
||||
|
||||
if (parsedUrl.pathname === '/callback') {
|
||||
const state = parsedUrl.query.state;
|
||||
// Implicit flow: access_token comes as a query param
|
||||
const accessToken = parsedUrl.query.access_token;
|
||||
// Code flow fallback
|
||||
const code = parsedUrl.query.code;
|
||||
const state = parsedUrl.query.state;
|
||||
if (state !== expectedState) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>Authentication Failed</h1><p>Invalid state.</p>');
|
||||
server.close();
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state !== expectedState) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>Authentication Failed</h1><p>Invalid state.</p>');
|
||||
server.close();
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const code = parsedUrl.query.code;
|
||||
const directToken = parsedUrl.query.access_token;
|
||||
|
||||
if (accessToken) {
|
||||
if (code && codeVerifier) {
|
||||
// Authorization Code + PKCE exchange
|
||||
try {
|
||||
const body: Record<string, string> = {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
code: code as string,
|
||||
redirect_uri: 'http://localhost:8765/callback',
|
||||
code_verifier: codeVerifier,
|
||||
};
|
||||
if (clientSecret) body.client_secret = clientSecret;
|
||||
|
||||
const tokenResp = await axios.post(`${endpoint}/api/login/oauth/access_token`, body);
|
||||
const tokens = tokenResp.data;
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`<html><body style="font-family:system-ui;text-align:center;padding:50px">
|
||||
<h1 style="color:#10b981">Authentication Successful!</h1>
|
||||
<p>You can close this window.</p>
|
||||
<script>setTimeout(()=>window.close(),3000)</script></body></html>`);
|
||||
res.end(successHtml);
|
||||
server.close();
|
||||
const expiresIn = parsedUrl.query.expires_in ? parseInt(parsedUrl.query.expires_in, 10) : undefined;
|
||||
resolve({
|
||||
accessToken: accessToken as string,
|
||||
refreshToken: parsedUrl.query.refresh_token as string | undefined,
|
||||
expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
|
||||
});
|
||||
} else if (code) {
|
||||
// Fallback: extract code for code exchange (handled upstream if needed)
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`<html><body style="font-family:system-ui;text-align:center;padding:50px">
|
||||
<h1 style="color:#10b981">Authentication Successful!</h1>
|
||||
<p>You can close this window.</p></body></html>`);
|
||||
server.close();
|
||||
// For now, cannot exchange code here — implicit flow is preferred
|
||||
resolve(null);
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>Authentication Failed</h1><p>No token received.</p>');
|
||||
} catch (err: any) {
|
||||
res.writeHead(500, { 'Content-Type': 'text/html' });
|
||||
res.end(`<h1>Token Exchange Failed</h1><p>${err.message}</p>`);
|
||||
server.close();
|
||||
resolve(null);
|
||||
}
|
||||
} else if (directToken) {
|
||||
// Implicit flow fallback
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(successHtml);
|
||||
server.close();
|
||||
const expiresIn = parsedUrl.query.expires_in ? parseInt(parsedUrl.query.expires_in, 10) : undefined;
|
||||
resolve({
|
||||
accessToken: directToken as string,
|
||||
refreshToken: parsedUrl.query.refresh_token as string | undefined,
|
||||
expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,
|
||||
});
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>Authentication Failed</h1><p>No code or token received.</p>');
|
||||
server.close();
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(8765, 'localhost');
|
||||
|
||||
setTimeout(() => {
|
||||
server.close();
|
||||
resolve(null);
|
||||
}, 300000);
|
||||
setTimeout(() => { server.close(); resolve(null); }, 300000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user