-
Notifications
You must be signed in to change notification settings - Fork 4
feature: Oauth authentication #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Cragsmann
wants to merge
6
commits into
functions-dev:master
from
Cragsmann:SRVOCF-856--oauth-authentication
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
264b9f8
SRVOCF-862: added disabled github auth button and divider
Cragsmann 3e86a31
SRVOCF-856: Add OAuth authentication
Cragsmann fefbadf
SRVOCF-856: fix lint styling
Cragsmann 0054d12
SRVOCF-856: update tooltip content on disabled auth button
Cragsmann f86e356
SRVOCF-856: fix review comments
Cragsmann c1a8cdd
SRVOCF-856: extract ouath_handler.go and add test for OAuthService
Cragsmann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,112 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "regexp" | ||
| "strings" | ||
| ) | ||
|
|
||
| var ( | ||
| githubClientID = os.Getenv("GITHUB_CLIENT_ID") | ||
| githubClientSecret = os.Getenv("GITHUB_CLIENT_SECRET") | ||
| ) | ||
|
|
||
| func handleOAuthConfig(w http.ResponseWriter, _ *http.Request) { | ||
| enabled := githubClientID != "" && githubClientSecret != "" | ||
| cid := "" | ||
| if enabled { | ||
| cid = githubClientID | ||
| } | ||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(map[string]any{ | ||
| "client_id": cid, | ||
| "enabled": enabled, | ||
| }) | ||
| } | ||
|
|
||
| type oauthCallbackRequest struct { | ||
| Code string `json:"code"` | ||
| CodeVerifier string `json:"code_verifier"` | ||
| } | ||
|
|
||
| var validOAuthParam = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`) | ||
|
|
||
| func handleOAuthCallback(w http.ResponseWriter, r *http.Request) { | ||
| if githubClientID == "" || githubClientSecret == "" { | ||
| jsonError(w, "OAuth is not configured on this server", http.StatusServiceUnavailable) | ||
| return | ||
| } | ||
|
|
||
| var req oauthCallbackRequest | ||
| r.Body = http.MaxBytesReader(w, r.Body, 4096) | ||
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||
| jsonError(w, "invalid request body: "+err.Error(), http.StatusBadRequest) | ||
| return | ||
| } | ||
| if !validOAuthParam.MatchString(req.Code) { | ||
| jsonError(w, "invalid authorization code", http.StatusBadRequest) | ||
| return | ||
| } | ||
| if !validOAuthParam.MatchString(req.CodeVerifier) { | ||
| jsonError(w, "invalid code verifier", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| form := url.Values{ | ||
| "client_id": {githubClientID}, | ||
| "client_secret": {githubClientSecret}, | ||
| "code": {req.Code}, | ||
| "code_verifier": {req.CodeVerifier}, | ||
| } | ||
| ghReq, err := http.NewRequestWithContext(r.Context(), "POST", "https://github.com/login/oauth/access_token", strings.NewReader(form.Encode())) | ||
| if err != nil { | ||
| jsonError(w, "failed to build token request", http.StatusInternalServerError) | ||
| return | ||
| } | ||
| ghReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
| ghReq.Header.Set("Accept", "application/json") | ||
|
|
||
| resp, err := http.DefaultClient.Do(ghReq) | ||
| if err != nil { | ||
| jsonError(w, "failed to exchange token with GitHub: "+err.Error(), http.StatusBadGateway) | ||
| return | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| jsonError(w, fmt.Sprintf("GitHub returned HTTP %d", resp.StatusCode), http.StatusBadGateway) | ||
| return | ||
| } | ||
|
|
||
| body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<16)) | ||
| if err != nil { | ||
| jsonError(w, "failed to read GitHub response", http.StatusBadGateway) | ||
| return | ||
| } | ||
|
|
||
| var ghResp map[string]any | ||
| if err := json.Unmarshal(body, &ghResp); err != nil { | ||
| jsonError(w, "invalid response from GitHub", http.StatusBadGateway) | ||
| return | ||
| } | ||
|
|
||
| if errMsg, ok := ghResp["error"]; ok { | ||
| desc, _ := ghResp["error_description"].(string) | ||
| jsonError(w, fmt.Sprintf("%v: %s", errMsg, desc), http.StatusUnauthorized) | ||
| return | ||
| } | ||
|
|
||
| token, _ := ghResp["access_token"].(string) | ||
| if token == "" { | ||
| jsonError(w, "no access_token in GitHub response", http.StatusBadGateway) | ||
| return | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(map[string]string{"access_token": token}) | ||
| } |
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,63 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { DisconnectConfirmModal } from './DisconnectConfirmModal'; | ||
|
|
||
| vi.mock('react-i18next', () => ({ | ||
| useTranslation: () => ({ t: (key: string) => key }), | ||
| })); | ||
|
|
||
| describe('DisconnectConfirmModal', () => { | ||
| const defaultProps = { | ||
| isOpen: true, | ||
| onClose: vi.fn(), | ||
| onConfirm: vi.fn(), | ||
| }; | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('renders title and confirmation text', () => { | ||
| render(<DisconnectConfirmModal {...defaultProps} />); | ||
|
|
||
| expect(screen.getByText('Disconnect from GitHub')).toBeInTheDocument(); | ||
| expect( | ||
| screen.getByText('Are you sure you want to disconnect from GitHub?'), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders Disconnect and Cancel buttons', () => { | ||
| render(<DisconnectConfirmModal {...defaultProps} />); | ||
|
|
||
| expect(screen.getByRole('button', { name: 'Disconnect' })).toBeInTheDocument(); | ||
| expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('calls onConfirm when Disconnect is clicked', async () => { | ||
| const onConfirm = vi.fn(); | ||
| const user = userEvent.setup(); | ||
|
|
||
| render(<DisconnectConfirmModal {...defaultProps} onConfirm={onConfirm} />); | ||
|
|
||
| await user.click(screen.getByRole('button', { name: 'Disconnect' })); | ||
|
|
||
| expect(onConfirm).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it('calls onClose when Cancel is clicked', async () => { | ||
| const onClose = vi.fn(); | ||
| const user = userEvent.setup(); | ||
|
|
||
| render(<DisconnectConfirmModal {...defaultProps} onClose={onClose} />); | ||
|
|
||
| await user.click(screen.getByRole('button', { name: 'Cancel' })); | ||
|
|
||
| expect(onClose).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it('does not render content when closed', () => { | ||
| render(<DisconnectConfirmModal {...defaultProps} isOpen={false} />); | ||
|
|
||
| expect(screen.queryByText('Disconnect from GitHub')).not.toBeInTheDocument(); | ||
| }); | ||
| }); |
This file contains hidden or 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,31 @@ | ||
| import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| interface DisconnectConfirmModalProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| onConfirm: () => void; | ||
| } | ||
|
|
||
| export function DisconnectConfirmModal({ | ||
| isOpen, | ||
| onClose, | ||
| onConfirm, | ||
| }: DisconnectConfirmModalProps) { | ||
| const { t } = useTranslation('plugin__console-functions-plugin'); | ||
|
|
||
| return ( | ||
| <Modal isOpen={isOpen} onClose={onClose} variant="small"> | ||
| <ModalHeader title={t('Disconnect from GitHub')} /> | ||
| <ModalBody>{t('Are you sure you want to disconnect from GitHub?')}</ModalBody> | ||
| <ModalFooter> | ||
| <Button variant="danger" onClick={onConfirm}> | ||
| {t('Disconnect')} | ||
| </Button> | ||
| <Button variant="link" onClick={onClose}> | ||
| {t('Cancel')} | ||
| </Button> | ||
| </ModalFooter> | ||
| </Modal> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this all hard-wired during installation? Could we make it configurable via some ConfigMap? So admin can enable it post-installation by modifying some configmap and then doing deployment roll-out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enabling GitHub OAuth (post-installation)
OAuth is disabled by default. The plugin determines whether OAuth is active
based on the presence of a non-empty
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRET. To enable it after installation, follow the stepsbelow.
Replace
console-functions-pluginwith the namespace where the plugin is installed andconsole-functions-pluginwith the Helm release name (e.g.console-functions-plugin).1. Create a GitHub OAuth App
(direct link: https://github.com/settings/applications/new).
(e.g.
https://console-openshift-console.apps.<cluster-domain>).callback path appended, e.g.
https://console-openshift-console.apps.<cluster-domain>/faas/oauth/callback.Store it securely; GitHub will not show it again.
2. Create the Secret
If you used a custom secret name or key during installation
(
plugin.oauth.githubSecretName/plugin.oauth.githubSecretKey),use those values instead of
github-oauthandclient-secret.3. Set the Client ID in the ConfigMap
oc patch configmap console-functions-plugin-oauth \ -n console-functions-plugin \ --type merge \ -p '{"data":{"GITHUB_CLIENT_ID":"<YOUR_CLIENT_ID>"}}'4. Restart the pods
The deployment has a checksum annotation that triggers a rollout
automatically on
helm upgrade. After a manual ConfigMap edit you needto restart the pods yourself:
5. Verify
Check the plugin UI. The "Sign in with GitHub" button should now be active.
Disabling OAuth
To disable OAuth again, clear the Client ID:
oc patch configmap console-functions-plugin-oauth \ -n console-functions-plugin \ --type merge \ -p '{"data":{"GITHUB_CLIENT_ID":""}}' oc rollout restart deployment/console-functions-plugin -n console-functions-pluginYou can optionally delete the Secret as well: