-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from echohello-dev/feature/github-login
feat: GitHub Login
- Loading branch information
Showing
9 changed files
with
1,383 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
}, | ||
}); | ||
}, | ||
}), | ||
}); | ||
}, | ||
}); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.