mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
refactor oauth for discovery and dcr
This commit is contained in:
parent
47ac8222cd
commit
642dd7a167
13 changed files with 661 additions and 300 deletions
|
|
@ -2,10 +2,12 @@ import { BrowserWindow } from 'electron';
|
|||
import { randomBytes } from 'crypto';
|
||||
import { createAuthServer } from './auth-server.js';
|
||||
import { generateCodeVerifier, generateCodeChallenge } from '@x/core/dist/auth/pkce.js';
|
||||
import { createOAuthService } from '@x/core/dist/auth/oauth.js';
|
||||
import { getAvailableProviders } from '@x/core/dist/auth/providers.js';
|
||||
import { OAuthService } from '@x/core/dist/auth/oauth.js';
|
||||
import { getProviderConfig, getAvailableProviders } from '@x/core/dist/auth/providers.js';
|
||||
import { discoverAuthorizationServer, createStaticMetadata, AuthorizationServerMetadata } from '@x/core/dist/auth/discovery.js';
|
||||
import container from '@x/core/dist/di/container.js';
|
||||
import { IOAuthRepo } from '@x/core/dist/auth/repo.js';
|
||||
import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js';
|
||||
|
||||
const REDIRECT_URI = 'http://localhost:8080/oauth/callback';
|
||||
|
||||
|
|
@ -26,13 +28,106 @@ function getOAuthRepo(): IOAuthRepo {
|
|||
return container.resolve<IOAuthRepo>('oauthRepo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client registration repository from DI container
|
||||
*/
|
||||
function getClientRegistrationRepo(): IClientRegistrationRepo {
|
||||
return container.resolve<IClientRegistrationRepo>('clientRegistrationRepo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover or get provider metadata
|
||||
*/
|
||||
async function getProviderMetadata(provider: string): Promise<AuthorizationServerMetadata> {
|
||||
const config = getProviderConfig(provider);
|
||||
|
||||
if (config.discovery.mode === 'issuer') {
|
||||
// Discover endpoints from well-known
|
||||
console.log(`[OAuth] Discovering metadata for ${provider} from issuer: ${config.discovery.issuer}`);
|
||||
return await discoverAuthorizationServer(config.discovery.issuer);
|
||||
} else {
|
||||
// Use static endpoints
|
||||
console.log(`[OAuth] Using static metadata for ${provider} (no discovery)`);
|
||||
return createStaticMetadata(
|
||||
config.discovery.authorizationEndpoint,
|
||||
config.discovery.tokenEndpoint,
|
||||
config.discovery.revocationEndpoint
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or register client ID based on provider configuration
|
||||
*/
|
||||
async function getOrRegisterClient(
|
||||
provider: string,
|
||||
metadata: AuthorizationServerMetadata,
|
||||
scopes: string[]
|
||||
): Promise<string> {
|
||||
const config = getProviderConfig(provider);
|
||||
const clientRepo = getClientRegistrationRepo();
|
||||
|
||||
if (config.client.mode === 'static') {
|
||||
// Use static client ID
|
||||
if (!config.client.clientId) {
|
||||
throw new Error('Static client mode requires clientId in provider configuration');
|
||||
}
|
||||
console.log(`[OAuth] Using static client ID for ${provider}`);
|
||||
return config.client.clientId;
|
||||
} else {
|
||||
// DCR mode - check if registration endpoint exists
|
||||
const registrationEndpoint = config.client.registrationEndpoint || metadata.registration_endpoint;
|
||||
|
||||
if (!registrationEndpoint) {
|
||||
throw new Error('Provider does not support Dynamic Client Registration (no registration_endpoint found)');
|
||||
}
|
||||
|
||||
// Check for existing registered client
|
||||
const existingRegistration = await clientRepo.getClientRegistration(provider);
|
||||
if (existingRegistration) {
|
||||
console.log(`[OAuth] Using existing DCR client registration for ${provider}`);
|
||||
return existingRegistration.client_id;
|
||||
}
|
||||
|
||||
// Register new client - create temporary service just for registration
|
||||
// We need to pass a dummy clientId, but it won't be used for registration
|
||||
console.log(`[OAuth] Registering new client via DCR for ${provider}...`);
|
||||
const tempService = new OAuthService(metadata, 'temp', scopes);
|
||||
const registration = await tempService.registerClient([REDIRECT_URI], scopes);
|
||||
|
||||
// Save registration
|
||||
await clientRepo.saveClientRegistration(provider, registration);
|
||||
console.log(`[OAuth] DCR registration successful for ${provider}, client_id: ${registration.client_id}`);
|
||||
|
||||
return registration.client_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate OAuth flow for a provider
|
||||
*/
|
||||
export async function connectProvider(provider: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const oauthService = createOAuthService(provider);
|
||||
console.log(`[OAuth] Starting connection flow for ${provider}...`);
|
||||
const oauthRepo = getOAuthRepo();
|
||||
const config = getProviderConfig(provider);
|
||||
|
||||
// Validate configuration combinations
|
||||
if (config.discovery.mode === 'static' && config.client.mode === 'dcr') {
|
||||
throw new Error('DCR requires discovery mode "issuer", not "static"');
|
||||
}
|
||||
|
||||
// Get provider metadata (discover or use static)
|
||||
const metadata = await getProviderMetadata(provider);
|
||||
|
||||
// Get scopes from config or use empty array
|
||||
const scopes = config.scopes || [];
|
||||
|
||||
// Get or register client ID
|
||||
const clientId = await getOrRegisterClient(provider, metadata, scopes);
|
||||
|
||||
// Create OAuth service with metadata and client ID
|
||||
const oauthService = new OAuthService(metadata, clientId, scopes);
|
||||
|
||||
// Generate PKCE codes
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
|
|
@ -56,6 +151,7 @@ export async function connectProvider(provider: string): Promise<{ success: bool
|
|||
|
||||
try {
|
||||
// Exchange code for tokens
|
||||
console.log(`[OAuth] Exchanging authorization code for tokens (${provider})...`);
|
||||
const tokens = await oauthService.exchangeCodeForTokens(
|
||||
code,
|
||||
flow.codeVerifier,
|
||||
|
|
@ -63,6 +159,7 @@ export async function connectProvider(provider: string): Promise<{ success: bool
|
|||
);
|
||||
|
||||
// Save tokens
|
||||
console.log(`[OAuth] Token exchange successful for ${provider}`);
|
||||
await oauthRepo.saveTokens(provider, tokens);
|
||||
} catch (error) {
|
||||
console.error('OAuth token exchange failed:', error);
|
||||
|
|
@ -142,13 +239,23 @@ export async function isConnected(provider: string): Promise<{ isConnected: bool
|
|||
export async function getAccessToken(provider: string): Promise<string | null> {
|
||||
try {
|
||||
const oauthRepo = getOAuthRepo();
|
||||
const oauthService = createOAuthService(provider);
|
||||
const config = getProviderConfig(provider);
|
||||
|
||||
let tokens = await oauthRepo.getTokens(provider);
|
||||
if (!tokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get provider metadata
|
||||
const metadata = await getProviderMetadata(provider);
|
||||
|
||||
// Get client ID (static or registered)
|
||||
const clientId = await getOrRegisterClient(provider, metadata, config.scopes || []);
|
||||
|
||||
// Create OAuth service
|
||||
const scopes = config.scopes || [];
|
||||
const oauthService = new OAuthService(metadata, clientId, scopes);
|
||||
|
||||
// Check if token needs refresh
|
||||
if (oauthService.isTokenExpired(tokens)) {
|
||||
if (!tokens.refresh_token) {
|
||||
|
|
@ -158,7 +265,7 @@ export async function getAccessToken(provider: string): Promise<string | null> {
|
|||
|
||||
try {
|
||||
// Refresh token, preserving existing scopes
|
||||
const existingScopes = (tokens).scopes;
|
||||
const existingScopes = tokens.scopes;
|
||||
tokens = await oauthService.refreshAccessToken(tokens.refresh_token, existingScopes);
|
||||
await oauthRepo.saveTokens(provider, tokens);
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue