Skip to content

Commit

Permalink
Merge pull request #19 from echohello-dev/feature/github-login
Browse files Browse the repository at this point in the history
feat: GitHub Login
  • Loading branch information
johnnyhuy committed Jul 28, 2024
2 parents fdf6a33 + 3ef56a5 commit 849d1a5
Show file tree
Hide file tree
Showing 9 changed files with 1,383 additions and 29 deletions.
17 changes: 17 additions & 0 deletions app-config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,24 @@ catalog:
target: ../../examples/all.yaml
- type: file
target: ../../examples/acme-corp.yaml
## Uncomment the following to enable Plausible analytics in development mode
# plausible:
# enabled: true
# dataDomain: backstage.localhost
# sourceUrl: http://plausible.localhost/js/script.js

## Uncomment the following to enable GitHub authentication in development mode
# auth:
# environment: development
# providers:
# guest:
# dangerouslyAllowOutsideDevelopment: true
# github:
# development:
# clientId: ${GITHUB_CLIENT_ID}
# clientSecret: ${GITHUB_CLIENT_SECRET}
# signIn:
# resolvers:
# # Matches the GitHub username with the Backstage user entity name.
# # See https://backstage.io/docs/auth/github/provider#resolvers for more resolvers.
# - resolver: usernameMatchingUserEntityName
10 changes: 10 additions & 0 deletions app-config.production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,19 @@ backend:
credentials: true

auth:
environment: production
providers:
guest:
dangerouslyAllowOutsideDevelopment: true
github:
production:
clientId: ${GITHUB_CLIENT_ID}
clientSecret: ${GITHUB_CLIENT_SECRET}
signIn:
resolvers:
# Matches the GitHub username with the Backstage user entity name.
# See https://backstage.io/docs/auth/github/provider#resolvers for more resolvers.
- resolver: usernameMatchingUserEntityName
catalog:
# Overrides the default list locations from app-config.yaml as these contain example data.
# See https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog for more details
Expand Down
110 changes: 110 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Auth

Backstage provides a number of authentication strategies out of the box. These can be configured in the `app-config.yaml` file.

Refer to the [Backstage documentation](https://backstage.io/docs/auth/) for more information.

Since the [new backend](https://backstage.io/docs/backend-system/building-backends/index/) was introduced, authentication plugins have been abstracted away into a separate package. To reference the new authentication plugins, use the following import:

```typescript title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
```

Refer to [Migrating to New Auth Services](https://backstage.io/docs/tutorials/auth-service-migration#migrating-the-backend) documentation for more information.

## This Project

This project will use GitHub and Guest authentication strategies.

### GitHub

To enable GitHub authentication, add the following configuration to the `app-config.yaml` file:

```diff
auth:
+ environment: development
providers:
+ github:
+ development:
+ clientId: <client-id>
+ clientSecret: <client-secret>
+ signIn:
+ resolvers:
+ - resolver: emailMatchingUserEntityProfileEmail
```

Replace `<client-id>` and `<client-secret>` with the values from your GitHub OAuth application.

See [Backstage documentation](https://backstage.io/docs/auth/github/provider#resolvers) for more resolvers.

Note that the `environment` key is set to `development` to allow for the use of GitHub authentication in a local development environment. This can be changed to `production` for a production environment.

#### Additional Scopes

The `additionalScopes` key is used to request additional permissions from the user. In this case, we are requesting the `user:email` and `read:org` scopes.

```diff
auth:
+ environment: development
providers:
+ github:
+ development:
+ clientId: <client-id>
+ clientSecret: <client-secret>
+ additionalScopes:
+ - user:email
+ - read:org
+ signIn:
+ resolvers:
+ - resolver: emailMatchingUserEntityProfileEmail
```

We can then query the users email and organisations using the fetch method:

```typescript
createOAuthProviderFactory({
authenticator: githubAuthenticator,
async signInResolver(
{profile, result},
ctx,
): Promise<BackstageSignInResult> {
const emailResponse = await fetch(
'https://api.github.com/user/emails',
{
headers: {
Authorization: `Bearer ${result.session.accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
},
);
const emails = await emailResponse.json();

// Fetch user's organizations
const orgsResponse = await fetch(
'https://api.github.com/user/orgs',
{
headers: {
Authorization: `Bearer ${result.session.accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
},
);
const orgs = await orgsResponse.json();
})
})
```

### Guest

To enable Guest authentication, add the following configuration to the `app-config.yaml` file:

```diff
auth:
providers:
+ guest:
+ dangerouslyAllowOutsideDevelopment: true
```

This will allow users to access the Backstage application without authenticating.
17 changes: 16 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { backstageTheme } from './theme';
import { HomePage } from './components/home/HomePage';
import CssBaseline from '@mui/material/CssBaseline';
import { PlausibleAnalytics } from '@internal/backstage-plugin-plausible';
import { githubAuthApiRef } from '@backstage/core-plugin-api';

const app = createApp({
apis,
Expand All @@ -63,7 +64,21 @@ const app = createApp({
});
},
components: {
SignInPage: props => <SignInPage {...props} auto providers={['guest']} />,
SignInPage: props => (
<SignInPage
{...props}
auto
providers={[
'guest',
{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
},
]}
/>
),
},
themes: [
{
Expand Down
11 changes: 9 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
"dependencies": {
"@backstage/backend-common": "^0.23.2",
"@backstage/backend-defaults": "^0.3.3",
"@backstage/backend-plugin-api": "^0.7.0",
"@backstage/backend-tasks": "^0.5.26",
"@backstage/catalog-model": "^1.5.0",
"@backstage/config": "^1.2.0",
"@backstage/plugin-app-backend": "^0.3.70",
"@backstage/plugin-auth-backend": "^0.22.8",
"@backstage/plugin-auth-backend-module-github-provider": "^0.1.18",
"@backstage/plugin-auth-backend-module-github-provider": "^0.1.19",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.1.7",
"@backstage/plugin-auth-node": "^0.4.16",
"@backstage/plugin-catalog-backend": "^1.23.2",
Expand All @@ -46,11 +48,16 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.4.4",
"@backstage/cli": "^0.26.10",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4"
"@types/luxon": "^2.0.4",
"@types/supertest": "^6.0.2",
"jose": "^5.6.3",
"msw": "^2.3.4",
"supertest": "^7.0.0"
},
"files": [
"dist"
Expand Down
100 changes: 100 additions & 0 deletions packages/backend/src/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import request from 'supertest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import {
mockServices,
registerMswTestHooks,
startTestBackend,
} from '@backstage/backend-test-utils';
import { Server } from 'http';
import { githubAuth } from './auth';

describe('githubAuth', () => {
let server: Server;
let baseUrl: string;

const mockGithubAuth = setupServer(
// Mock GitHub's OAuth endpoints
http.get('https://github.com/login/oauth/authorize', params => {
const url = new URL(params.request.url);
const callbackUrl = new URL(url.searchParams.get('redirect_uri')!);
callbackUrl.searchParams.set('code', 'github_auth_code');
callbackUrl.searchParams.set('state', url.searchParams.get('state')!);
return HttpResponse.redirect(callbackUrl.toString());
}),

http.post('https://github.com/login/oauth/access_token', () => {
return HttpResponse.json({
access_token: 'github_access_token',
token_type: 'bearer',
scope: 'read:user',
});
}),

http.get('https://api.github.com/user', () => {
return HttpResponse.json({
login: 'octocat',
name: 'Octocat',
email: 'octocat@github.com',
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
});
}),
);

registerMswTestHooks(mockGithubAuth);

beforeAll(async () => {
const backend = await startTestBackend({
features: [
githubAuth,
import('@backstage/plugin-auth-backend'),
mockServices.rootConfig.factory({
data: {
app: { baseUrl: 'http://localhost:3000' },
auth: {
providers: {
github: {
development: {
clientId: 'github-client-id',
clientSecret: 'github-client-secret',
},
},
},
},
},
}),
],
});

server = backend.server;
baseUrl = `http://localhost:${backend.server.port()}`;
mockGithubAuth.listen();
});

afterAll(() => {
server.close();
});

it('should complete the GitHub auth flow', async () => {
// Start the auth flow
const startResponse = await request(server)
.get('/api/auth/github/start?env=development')
.expect(302);

const startUrl = new URL(startResponse.header.location);
expect(startUrl.searchParams.get('response_type')).not.toBeNull();
expect(startUrl.searchParams.get('redirect_uri')).not.toBeNull();

// Simulate the OAuth callback
const callbackUrl = new URL('/api/auth/github/handler/frame', baseUrl);
callbackUrl.searchParams.set('env', 'development');
callbackUrl.searchParams.set('code', 'github_auth_code');
callbackUrl.searchParams.set('state', 'mock_state');

const handlerResponse = await request(server).get(
callbackUrl.pathname + callbackUrl.search,
);

expect(handlerResponse.status).toBe(200);
});
});
54 changes: 54 additions & 0 deletions packages/backend/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
stringifyEntityRef,
DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { githubAuthenticator } from '@backstage/plugin-auth-backend-module-github-provider';
import {
authProvidersExtensionPoint,
BackstageSignInResult,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';

export const githubAuth = createBackendModule({
pluginId: 'auth',
moduleId: 'github',
register(reg) {
reg.registerInit({
deps: { providers: authProvidersExtensionPoint },
async init({ providers }) {
providers.registerProvider({
// This ID must match the actual provider config, e.g. addressing
// auth.providers.github means that this must be "github".
providerId: 'github',
// Use createProxyAuthProviderFactory instead if it's one of the proxy
// based providers rather than an OAuth based one
factory: createOAuthProviderFactory({
authenticator: githubAuthenticator,
async signInResolver(
{ result },
ctx,
): Promise<BackstageSignInResult> {
if (!result.fullProfile.username) {
throw new Error('Username not found in profile');
}

const userEntity = stringifyEntityRef({
kind: 'User',
name: result.fullProfile.username,
namespace: DEFAULT_NAMESPACE,
});

return ctx.issueToken({
claims: {
sub: userEntity,
ent: [userEntity],
},
});
},
}),
});
},
});
},
});
3 changes: 2 additions & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { createBackend } from '@backstage/backend-defaults';
import { githubAuth } from './auth';

const backend = createBackend();

Expand All @@ -19,7 +20,7 @@ backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
backend.add(import('@backstage/plugin-auth-backend'));
// See https://backstage.io/docs/backend-system/building-backends/migrating#the-auth-plugin
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
// See https://backstage.io/docs/auth/guest/provider
backend.add(githubAuth);

// catalog plugin
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
Expand Down
Loading

0 comments on commit 849d1a5

Please sign in to comment.