Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Step 1: Define Minimal Scope in Client Metadata
- // Secure OAuth client metadata
- function buildClientMetadata() {
- const baseMetadata = {
- application_type: 'web',
- client_name: 'Bluesky Tool',
- dpop_bound_access_tokens: true,
- grant_types: ['authorization_code'],
- response_types: ['code'],
- scope: 'atproto', // <-- Notice: only 'atproto' is requested
- token_endpoint_auth_method: 'private_key_jwt',
- token_endpoint_auth_signing_alg: 'ES256'
- };
- Step 2: Request Only the Base Scope During Login
- export async function initiateBlueskyOAuth(req: Request, res: Response) {
- // ...
- // ... (Production: Use PAR flow)
- const parPayload = new URLSearchParams({
- response_type: 'code',
- client_id: FINAL_CLIENT_METADATA_URL,
- redirect_uri: FINAL_REDIRECT_URI,
- scope: 'atproto', // <-- Explicitly 'atproto' for production
- state: state,
- code_challenge: codeChallenge,
- code_challenge_method: 'S256'
- });
- // ...
- // ... (Development: Use direct authorization flow)
- const authUrl = new URL(oauthMetadata.authorization_endpoint);
- authUrl.searchParams.set('response_type', 'code');
- authUrl.searchParams.set('client_id', FINAL_CLIENT_METADATA_URL);
- authUrl.searchParams.set('redirect_uri', FINAL_REDIRECT_URI);
- authUrl.searchParams.set('scope', 'atproto'); // <-- Explicitly 'atproto' for development
- authUrl.searchParams.set('state', state);
- authUrl.searchParams.set('code_challenge', codeChallenge);
- authUrl.searchParams.set('code_challenge_method', 'S256');
- res.redirect(authUrl.toString());
- // ...
- You've got it. Here is how the codebase implements that exact "authentication-only" flow, broken down into four steps with the specific TypeScript code.
- How It's Done in Your Codebase:
- The process separates "who you are" (Bluesky login) from "what you're allowed to do" (the app's own user list).
- Step 1: Define Minimal Scope in Client Metadata
- First, the application's metadata (what it tells Bluesky it's capable of) is configured to only include the base atproto scope. It does not include transition:generic or any other scopes that grant permissions.
- File: SkySight-main/server/routes/oauth.ts
- TypeScript
- // --- SECURITY FIX ---
- // Secure OAuth client metadata - no hardcoded insecure defaults
- function buildClientMetadata() {
- const baseMetadata = {
- application_type: 'web',
- client_name: 'Bluesky OSINT Tool',
- dpop_bound_access_tokens: true,
- grant_types: ['authorization_code'],
- response_types: ['code'],
- scope: 'atproto', // <-- Notice: only 'atproto' is requested
- token_endpoint_auth_method: 'private_key_jwt',
- token_endpoint_auth_signing_alg: 'ES256'
- };
- // ... rest of the function
- }
- Step 2: Request Only the Base Scope During Login
- When the user is redirected to log in, the code again explicitly requests only the atproto scope, ensuring it doesn't ask for more permissions at runtime.
- File: SkySight-main/server/blueskyAuth.ts
- TypeScript
- export async function initiateBlueskyOAuth(req: Request, res: Response) {
- // ...
- // ... (Production: Use PAR flow)
- const parPayload = new URLSearchParams({
- response_type: 'code',
- client_id: FINAL_CLIENT_METADATA_URL,
- redirect_uri: FINAL_REDIRECT_URI,
- scope: 'atproto', // <-- Explicitly 'atproto' for production
- state: state,
- code_challenge: codeChallenge,
- code_challenge_method: 'S256'
- });
- // ...
- // ... (Development: Use direct authorization flow)
- const authUrl = new URL(oauthMetadata.authorization_endpoint);
- authUrl.searchParams.set('response_type', 'code');
- authUrl.searchParams.set('client_id', FINAL_CLIENT_METADATA_URL);
- authUrl.searchParams.set('redirect_uri', FINAL_REDIRECT_URI);
- authUrl.searchParams.set('scope', 'atproto'); // <-- Explicitly 'atproto' for development
- authUrl.searchParams.set('state', state);
- authUrl.searchParams.set('code_challenge', codeChallenge);
- authUrl.searchParams.set('code_challenge_method', 'S256');
- res.redirect(authUrl.toString());
- // ...
- }
- Step 3: Use the Token Only to Get Profile/Identity
- export async function handleBlueskyOAuthCallback(req: Request, res: Response) {
- // ... (token exchange logic) ...
- // Get user profile information using access token
- let userProfile;
- try {
- const profileResponse = await fetch(`${pdsUrl}/xrpc/app.bsky.actor.getProfile?actor=${did}`, {
- headers: {
- 'Authorization': `Bearer ${tokens.access_token}`,
- },
- });
- if (profileResponse.ok) {
- userProfile = await profileResponse.json();
- }
- // ... (fallback logic) ...
- } catch (profileError) {
- // ...
- return res.redirect('/login?error=profile_fetch_failed');
- }
- // ... (next step) ...
- }
- Step 4: Check Identity Against an Internal Authorization List
- // ... (continuation of handleBlueskyOAuthCallback)
- // Check if user is authorized
- const isAuthorized = await isAuthorizedDID(userProfile.did);
- if (!isAuthorized) {
- console.log(`Unauthorized access attempt from DID: ${userProfile.did}`);
- return res.redirect('/login?error=unauthorized');
- }
- // Store user session (ONLY if they passed the isAuthorized check)
- session.user = {
- did: userProfile.did,
- handle: userProfile.handle,
- // ... (other user info)
- };
- // Update last login time
- await storage.updateAuthorizedUserLogin(userProfile.did);
- // ... (redirect to the app) ...
- }
Advertisement
Add Comment
Please, Sign In to add comment