Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ dist
dist-types
coverage
.vscode
.coder.yaml
app-config.local.yaml
10 changes: 8 additions & 2 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ app:
organization:
name: Coder

coder:
deployment:
accessUrl: https://dev.coder.com
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe unused

oauth:
clientId: ${CODER_OAUTH_CLIENT_ID:-backstage}
clientSecret: ${CODER_OAUTH_CLIENT_SECRET:-change-me}
Comment on lines +8 to +13
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this should be here or somewhere else, but is useful for development.


backend:
# Used for enabling authentication, secret is shared by all backend plugins
# See https://backstage.io/docs/auth/service-to-service-auth for
Expand All @@ -15,8 +22,7 @@ backend:
baseUrl: http://localhost:7007
listen:
port: 7007
# Uncomment the following host directive to bind to specific interfaces
# host: 127.0.0.1
host: localhost
csp:
connect-src: ["'self'", 'http:', 'https:']
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"node": "18 || 20"
},
"scripts": {
"dev": "concurrently \"yarn dev-init\" \"yarn start\" \"yarn start-backend\"",
"dev-init": "/bin/bash ./scripts/dev-init.sh",
"dev": "yarn dev-init && concurrently --names \"react,backend\" -c \"green,blue\" \"yarn start\" \"yarn start-backend\"",
"dev-init": "./scripts/dev-init.sh",
"start": "yarn workspace app start",
"start-backend": "yarn workspace backend start",
"build:backend": "yarn workspace backend build",
Expand Down Expand Up @@ -55,5 +55,6 @@
"*.{json,md}": [
"prettier --write"
]
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
5 changes: 5 additions & 0 deletions packages/app/src/components/catalog/EntityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ const coderAppConfig: CoderAppConfig = {
accessUrl: 'https://dev.coder.com',
},

oauth: {
clientId: '09cd00cf-9517-401c-9601-3712f187b53c',
backendUrl: 'http://localhost:7007',
},
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been having trouble piping config from appConfig data into the frontend apps, so this is still hard coded to get things working for now.


workspaces: {
defaultTemplateName: 'devcontainers',
defaultMode: 'manual',
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@backstage/plugin-search-backend-module-techdocs": "^0.1.13",
"@backstage/plugin-search-backend-node": "^1.2.13",
"@backstage/plugin-techdocs-backend": "^1.9.2",
"@coder/backstage-plugin-coder-backend": "0.0.0",
"@coder/backstage-plugin-devcontainers-backend": "0.0.0",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import search from './plugins/search';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { createRouter as createCoderRouter } from '@coder/backstage-plugin-coder-backend';

function makeCreateEnv(config: Config) {
const root = getRootLogger();
Expand Down Expand Up @@ -85,10 +86,18 @@ async function main() {
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
const coderEnv = useHotMemoize(module, () => createEnv('coder'));

const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use(
'/auth/coder',
await createCoderRouter({
logger: coderEnv.logger,
config: coderEnv.config,
}),
);
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
Expand Down
1 change: 1 addition & 0 deletions plugins/backstage-plugin-coder-backend/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Comment thread
f0ssel marked this conversation as resolved.
49 changes: 49 additions & 0 deletions plugins/backstage-plugin-coder-backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@coder/backstage-plugin-coder-backend",
"description": "Backend plugin for Coder OAuth2 authentication flow",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-common": "^0.20.1",
"@backstage/config": "^1.1.1",
"@backstage/errors": "^1.2.3",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"winston": "^3.2.1",
"axios": "^1.6.8"
},
"devDependencies": {
"@backstage/cli": "^0.25.1",
"@types/supertest": "^2.0.12",
"supertest": "^6.2.4"
},
"files": [
"dist"
],
"keywords": [
"backstage",
"coder",
"oauth2",
"authentication"
]
}
7 changes: 7 additions & 0 deletions plugins/backstage-plugin-coder-backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Backend plugin for Coder OAuth2 authentication
*
* @packageDocumentation
*/

export { createRouter } from './service/router';
121 changes: 121 additions & 0 deletions plugins/backstage-plugin-coder-backend/src/service/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { errorHandler } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import axios from 'axios';

export interface RouterOptions {
logger: Logger;
config: Config;
}

export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, config } = options;

const router = Router();
router.use(express.json());

// OAuth callback endpoint
router.get('/oauth/callback', async (req, res) => {
const { code } = req.query;

if (!code || typeof code !== 'string') {
logger.error('OAuth callback missing authorization code');
res.status(400).send('Missing authorization code');
return;
}

try {
// Get Coder configuration from coder.oauth
const coderConfig = config.getOptionalConfig('coder');

const accessUrl = coderConfig?.getString('deployment.accessUrl') || '';
const clientId = coderConfig?.getString('oauth.clientId') || 'backstage';
const clientSecret = coderConfig?.getString('oauth.clientSecret') || '';
const redirectUri = `${req.protocol}://${req.get(
'host',
)}/api/auth/coder/oauth/callback`;

// Exchange authorization code for access token
const tokenResponse = await axios.post(
`${accessUrl}/oauth2/tokens`,
new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: clientId,
client_secret: clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);

const { access_token } = tokenResponse.data;

// Return HTML that sends the token to the opener window via postMessage
res.setHeader('Content-Security-Policy', "script-src 'unsafe-inline'");
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
</head>
<body>
<p>Authentication successful! This window will close automatically...</p>
<script>
(function() {
// Send token to opener window via postMessage
if (window.opener) {
var targetOrigin;
try {
// Try to get the opener's origin
targetOrigin = window.opener.location.origin;
} catch (e) {
// If we can't access it due to cross-origin, use wildcard
// This is safe since we're only sending to our own opener
targetOrigin = '*';
}

window.opener.postMessage(
{ type: 'coder-oauth-success', token: '${access_token}' },
targetOrigin
);
setTimeout(function() { window.close(); }, 500);
} else {
document.body.innerHTML = '<p>Authentication successful! You can close this window.</p>';
}
})();
</script>
</body>
</html>
`);

logger.info('OAuth authentication successful');
return;
} catch (error) {
logger.error('OAuth token exchange failed', error);
res
.status(500)
.send(
`<html><body><h1>Authentication failed</h1><p>${
error instanceof Error ? error.message : 'Unknown error'
}</p></body></html>`,
);
return;
}
});

router.get('/health', (_, response) => {
logger.info('Health check');
response.json({ status: 'ok' });
});

router.use(errorHandler());
return router;
}
6 changes: 6 additions & 0 deletions plugins/backstage-plugin-coder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ the Dev Container.
accessUrl: 'https://coder.example.com',
},

// Optional: OAuth configuration for "Sign in with Coder" button
// Get the clientId from your Coder OAuth2 apps settings
oauth: {
clientId: 'your-oauth-client-id',
},

// Set the default template (and parameters) for
// catalog items. Individual properties can be overridden
// by a repo's catalog-info.yaml file
Expand Down
28 changes: 28 additions & 0 deletions plugins/backstage-plugin-coder/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface Config {
/**
* @visibility frontend
*/
coder: {
/**
* @deepVisibility frontend
*/
deployment: {
accessUrl: string;
};

/**
* @visibility frontend
*/
oauth: {
/**
* @visibility frontend
*/
clientId: string;

/**
* @visibility secret
*/
clientSecret: string;
};
};
}
Comment on lines +1 to +28
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool I had no clue how to do this lol

5 changes: 5 additions & 0 deletions plugins/backstage-plugin-coder/dev/DevPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const appConfig: CoderAppConfig = {
accessUrl: 'https://dev.coder.com',
},

oauth: {
clientId: '09cd00cf-9517-401c-9601-3712f187b53c',
backendUrl: 'http://localhost:7007',
},
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same appConfig issue here


workspaces: {
defaultTemplateName: 'devcontainers',
defaultMode: 'manual',
Expand Down
8 changes: 5 additions & 3 deletions plugins/backstage-plugin-coder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"msw": "^1.0.0"
},
"files": [
"dist"
"dist",
"config.d.ts"
],
"keywords": [
"backstage",
Expand All @@ -73,5 +74,6 @@
"ide",
"vscode",
"jetbrains"
]
}
],
"configSchema": "config.d.ts"
}
Loading
Loading