Skip to content

feature: Oauth authentication#41

Closed
Cragsmann wants to merge 6 commits into
functions-dev:masterfrom
Cragsmann:SRVOCF-856--oauth-authentication
Closed

feature: Oauth authentication#41
Cragsmann wants to merge 6 commits into
functions-dev:masterfrom
Cragsmann:SRVOCF-856--oauth-authentication

Conversation

@Cragsmann

@Cragsmann Cragsmann commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Screen.Recording.2026-06-15.at.13.32.53.mov
  • Add full GitHub OAuth authentication with popup-based authorization code flow + PKCE
  • Keep PAT (Personal Access Token) as fallback authentication method
  • Add disconnect functionality via dropdown menu with confirmation modal
  • Split UserAvatar into smaller, testable components

Changes

Backend (backend/main.go)

  • GET /api/oauth/config: returns { client_id, enabled } from env vars
  • POST /api/oauth/callback: proxies authorization code + PKCE verifier to GitHub's token endpoint, returns access token

Frontend

  • OAuthService (src/common/services/oauth/): popup-based PKCE flow with state/CSRF validation, popup-close detection, and sessionStorage cleanup
  • OAuthCallbackPage (src/pages/oauth-callback/): minimal popup callback page that postMessages the authorization code back to the opener window
  • PatModal (src/common/components/PatModal.tsx): extracted from UserAvatar, OAuth button conditionally enabled based on backend config, "or" divider, PAT input
  • DisconnectConfirmModal (src/common/components/DisconnectConfirmModal.tsx): extracted confirmation dialog for disconnect action
  • UserAvatar (src/common/components/UserAvatar.tsx): logged-in state shows dropdown with disconnect option, not-logged-in state shows connect button + PatModal
  • Storage keys: unified TOKEN_KEY replaces hardcoded 'func-console-pat' across codebase, AUTH_METHOD_KEY tracks 'pat' | 'oauth'

Helm chart

  • values.yaml: plugin.oauth.enabled, githubClientId, githubSecretName, githubSecretKey
  • deployment.yaml: conditionally injects GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET (from K8s Secret) as env vars

Test plan

  • 147 tests passing (35 new tests for PatModal, DisconnectConfirmModal, and updated UserAvatar)
  • OAuth flow tested end-to-end: backend config, popup opens to GitHub, user authorizes, popup closes, user authenticated
  • PAT flow still works as fallback
  • Without OAuth env vars, button shows disabled with "Coming soon" tooltip
  • Disconnect clears token, user, and auth method from sessionStorage

Relates to SRVOCF-856

@Cragsmann Cragsmann requested review from matejvasek and twoGiants and removed request for twoGiants June 15, 2026 11:39

@matejvasek matejvasek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

via Claude:

Review

The architecture is solid: PKCE with server-side token exchange keeps the client secret off the frontend, the popup flow avoids full-page redirects in the console plugin context, and the component decomposition (PatModal, DisconnectConfirmModal) is a clear improvement. Good test coverage at 35 new tests.

Issues

1. Security: client_id exposed when OAuth is disabled

handleOAuthConfig returns client_id even when enabled is false. If a cluster has GITHUB_CLIENT_ID set but not GITHUB_CLIENT_SECRET, the client ID leaks to unauthenticated callers. Consider returning an empty client_id when enabled is false:

func handleOAuthConfig(w http.ResponseWriter, _ *http.Request) {
    enabled := githubClientID != "" && githubClientSecret != ""
    cid := ""
    if enabled {
        cid = githubClientID
    }
    // ...
}

2. Security: GitHub token response forwarded verbatim to the client

handleOAuthCallback does w.Write(body), forwarding the entire GitHub response body. GitHub's token response may include fields beyond access_token (e.g., scope, token_type, and potentially future fields). It would be safer to extract only access_token and return that:

token, _ := ghResp["access_token"].(string)
if token == "" {
    jsonError(w, "no access_token in GitHub response", http.StatusBadGateway)
    return
}
json.NewEncoder(w).Encode(map[string]string{"access_token": token})

3. Bug: PAT_KEY alias creates a migration trap

In types.ts:

export const TOKEN_KEY = 'func-console-token';
export const PAT_KEY = TOKEN_KEY;

The storage key changed from 'func-console-pat' to 'func-console-token', but there is no migration. Any user who authenticated before this PR will have their session silently dropped (their func-console-pat entry is orphaned). If that is intentional since this is a PoC, the PAT_KEY alias is misleading and should just be removed. If it is not intentional, a migration read of the old key would be needed.

4. Bug: ForgeConnectionProvider not notified on disconnect

disconnect() in useUserAvatar clears sessionStorage and local state, but never calls connectToForge (or an equivalent disconnect callback) on ForgeConnectionContext. This means ForgeConnectionContext.isActive remains true and connectionId is stale after disconnect, so downstream consumers (e.g., data-fetching hooks) won't know the user logged out until a full page refresh.

5. Singleton OAuthService caches config forever

OAuthService.#configCache is set once and never invalidated. If a cluster admin enables OAuth after the plugin is loaded, the user must do a full page refresh. This might be acceptable for a PoC, but worth noting.

6. Missing redirect_uri in token exchange

The handleOAuthCallback handler sends client_id, client_secret, code, and code_verifier to GitHub's token endpoint, but omits redirect_uri. GitHub's OAuth docs state that redirect_uri is required if it was included in the authorization request (and the frontend does include it). This may cause token exchange failures in some configurations. Worth verifying empirically, since GitHub may not strictly enforce it in all cases.

7. Missing error status code check for GitHub response

In handleOAuthCallback, the code reads the body and checks for a JSON error field, but never checks resp.StatusCode. If GitHub returns a non-200 with a non-JSON body, the json.Unmarshal will fail with a generic "invalid response from GitHub" instead of surfacing the actual HTTP status.

Nits

  • Inline style in PatModal: style={{ marginBottom: '1rem' }} on the Alert. Should use a PF utility class like pf-v6-u-mb-md for consistency with the rest of the PR (which already uses utility classes like pf-v6-u-my-md).
  • onOpenChange parameter naming: onOpenChange={(setOpen) => !setOpen && closeDropdown()} -- the parameter name setOpen is misleading since it is actually isOpen. Consider (open) => !open && closeDropdown().
  • Unused AuthMethod type: export type AuthMethod = 'pat' | 'oauth' is exported but never imported anywhere. The literal 'pat' | 'oauth' is used inline in finishLogin.
  • No tests for OAuthService or OAuthCallbackPage: The core PKCE/popup logic and the callback page rendering/postMessage behavior are untested.

Summary

The main items to address before merge are #3 (silent session drop / dead alias) and #4 (disconnect not propagated to context). Items #1, #2, and #6 are security/correctness fixes that are straightforward. The rest are minor.

@Cragsmann

Cragsmann commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review! Pushed fixes in f86e356. Here's the rundown:

Issues

# Item Status
1 Security: client_id exposed when OAuth is disabled Fixed. handleOAuthConfig now returns an empty client_id when enabled is false.
2 Security: GitHub token response forwarded verbatim Fixed. handleOAuthCallback now extracts only access_token and returns that.
3 Bug: PAT_KEY alias / migration trap Fixed. Removed the PAT_KEY alias and the unused AuthMethod type. All imports updated to use TOKEN_KEY directly. No migration needed since this is pre-release/PoC.
4 Bug: Disconnect not propagated to ForgeConnectionContext Fixed. Added disconnectFromForge to the context interface and provider, and UserAvatar.disconnect() now calls it to clear isActive and user.
5 Singleton OAuthService caches config forever Acknowledged, acceptable for PoC.
6 Missing redirect_uri in token exchange Verified empirically: GitHub does not enforce redirect_uri on the token exchange when code_verifier (PKCE) is present. Leaving as-is for now, can add if we hit issues.
7 Missing error status code check for GitHub response Fixed. Added resp.StatusCode != http.StatusOK check before reading the body.

Nits

  • Inline style in PatModal: Fixed, replaced with pf-v6-u-mb-md utility class.
  • onOpenChange parameter naming: Fixed, renamed setOpen to open.

Comment thread backend/main.go Outdated
@Cragsmann Cragsmann marked this pull request as ready for review June 16, 2026 07:39
@Cragsmann Cragsmann requested a review from matejvasek June 17, 2026 14:40
@twoGiants

Copy link
Copy Markdown
Collaborator

Hey @Cragsmann @matejvasek ! I see you did some substantial work here, thank you for that. I just skimmed it so far.

I don't want to invest the time for a full review as we have no confirmation yet if we'll need this feature.

I propose to close it but keep the PR link in Jira for reference. If it turns out we'll need this we reopen and I'll review.

Thoughts? @Cragsmann @matejvasek

@Cragsmann Cragsmann closed this Jun 19, 2026
protocol: TCP
args:
- "--https-port={{ .Values.plugin.port }}"
{{- if .Values.plugin.oauth.enabled }}

Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

diff --git a/charts/openshift-console-plugin/templates/configmap-oauth.yaml b/charts/openshift-console-plugin/templates/configmap-oauth.yaml
new file mode 100644
index 0000000..ceb4a4b
--- /dev/null
+++ b/charts/openshift-console-plugin/templates/configmap-oauth.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "openshift-console-plugin.name" . }}-oauth
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "openshift-console-plugin.labels" . | nindent 4 }}
+data:
+  GITHUB_CLIENT_ID: {{ .Values.plugin.oauth.githubClientId | quote }}
diff --git a/charts/openshift-console-plugin/templates/deployment.yaml b/charts/openshift-console-plugin/templates/deployment.yaml
index 37a79fe..8eaa70b 100644
--- a/charts/openshift-console-plugin/templates/deployment.yaml
+++ b/charts/openshift-console-plugin/templates/deployment.yaml
@@ -13,6 +13,8 @@ spec:
       {{- include "openshift-console-plugin.selectorLabels" . | nindent 6 }}
   template:
     metadata:
+      annotations:
+        checksum/oauth-config: {{ include (print $.Template.BasePath "/configmap-oauth.yaml") . | sha256sum }}
       labels:
             {{- include "openshift-console-plugin.labels" . | nindent 8 }}
     spec:
@@ -28,16 +30,19 @@ spec:
               protocol: TCP
           args:
             - "--https-port={{ .Values.plugin.port }}"
-          {{- if .Values.plugin.oauth.enabled }}
           env:
             - name: GITHUB_CLIENT_ID
-              value: {{ .Values.plugin.oauth.githubClientId | quote }}
+              valueFrom:
+                configMapKeyRef:
+                  name: {{ template "openshift-console-plugin.name" . }}-oauth
+                  key: GITHUB_CLIENT_ID
+                  optional: true
             - name: GITHUB_CLIENT_SECRET
               valueFrom:
                 secretKeyRef:
                   name: {{ .Values.plugin.oauth.githubSecretName }}
                   key: {{ .Values.plugin.oauth.githubSecretKey }}
-          {{- end }}
+                  optional: true
           imagePullPolicy: {{ .Values.plugin.imagePullPolicy }}
           {{- if and (.Values.plugin.securityContext.enabled) (.Values.plugin.containerSecurityContext) }}
           securityContext: {{ tpl (toYaml (omit .Values.plugin.containerSecurityContext "enabled")) $ | nindent 12 }}
diff --git a/charts/openshift-console-plugin/values.yaml b/charts/openshift-console-plugin/values.yaml
index 229bf23..e0e1c57 100644
--- a/charts/openshift-console-plugin/values.yaml
+++ b/charts/openshift-console-plugin/values.yaml
@@ -26,9 +26,8 @@ plugin:
       memory: 50Mi
   basePath: /
   oauth:
-    enabled: false
     githubClientId: ""
-    githubSecretName: ""
+    githubSecretName: "github-oauth"
     githubSecretKey: "client-secret"
   certificateSecretName: ""
   serviceAccount:

@matejvasek matejvasek Jun 22, 2026

Copy link
Copy Markdown
Collaborator

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_ID and
GITHUB_CLIENT_SECRET. To enable it after installation, follow the steps
below.

Replace console-functions-plugin with the namespace where the plugin is installed and
console-functions-plugin with the Helm release name (e.g. console-functions-plugin).

1. Create a GitHub OAuth App

  1. Go to GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
    (direct link: https://github.com/settings/applications/new).
  2. Fill in the form:
    • Application name -- any descriptive name (e.g. "OpenShift Functions Console").
    • Homepage URL -- the URL of your OpenShift Console
      (e.g. https://console-openshift-console.apps.<cluster-domain>).
    • Authorization callback URL -- the console URL with the plugin's
      callback path appended, e.g.
      https://console-openshift-console.apps.<cluster-domain>/faas/oauth/callback.
  3. Click Register application.
  4. On the next page, copy the Client ID.
  5. Click Generate a new client secret, then copy the generated value.
    Store it securely; GitHub will not show it again.

2. Create the Secret

oc create secret generic github-oauth \
  --from-literal=client-secret=<YOUR_CLIENT_SECRET> \
  -n console-functions-plugin

If you used a custom secret name or key during installation
(plugin.oauth.githubSecretName / plugin.oauth.githubSecretKey),
use those values instead of github-oauth and client-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 need
to restart the pods yourself:

oc rollout restart deployment/console-functions-plugin -n console-functions-plugin

5. Verify

oc rollout status deployment/console-functions-plugin -n console-functions-plugin

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-plugin

You can optionally delete the Secret as well:

oc delete secret github-oauth -n console-functions-plugin

@matejvasek

Copy link
Copy Markdown
Collaborator

While testing I had to do:

diff --git a/Dockerfile b/Dockerfile
index 6b9c593..f6f8f35 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,6 +27,7 @@ RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w" -o
 
 FROM registry.access.redhat.com/ubi9-micro:latest
 
+COPY --from=go-build /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
 COPY --from=go-build /opt/app-root/src/backend/plugin-backend /usr/bin/plugin-backend
 USER 1001
 

Otherwise backend would not trust github.com.

However this change should already be in master branch so it's fine.

@matejvasek

matejvasek commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

When trying to create a function I got an error:

{
  "message": "Not Found",
  "documentation_url": "https://docs.github.com/rest/git/trees#create-a-tree",
  "status": "404"
}

and the repository is in half-created state, see: https://github.com/matejvasek/fn-testing-abc

Perhaps some permissions are missing. But it also might be some race.

@matejvasek

Copy link
Copy Markdown
Collaborator

Findings: createTree 404 with OAuth tokens

While testing the OAuth flow, function creation failed with:

http code: 404 message: Not Found - https://docs.github.com/rest/git/trees#create-a-tree

The repo was created successfully, and createBlob, getRef, getCommit all succeeded on the new repo. Only createTree with base_tree (pointing to the auto_init commit's tree SHA) returned 404. This happened exclusively with OAuth tokens (gho_...), PATs worked fine.

What we tried

  1. Polling for ref availability (waitForRef with retries) before calling createTree. Did not help, the ref was available, getCommit returned the tree SHA, but createTree still 404'd on that same SHA as base_tree.

  2. Removing auto_init and pushing an orphan commit ourselves. This failed with 409 Git Repository is empty on createBlob, both with OAuth and PAT. GitHub's git data API (/git/blobs, /git/trees, etc.) requires at least one commit in the repo.

Fix

Keep auto_init: true so the git data API is functional, but call createTree without the base_tree parameter. This creates a standalone tree containing only the function template files (the auto_init README is intentionally replaced). The commit still references the auto_init commit as its parent, so commit history is clean.

// Before (404 with OAuth tokens):
const { data: tree } = await this.#octokit.git.createTree({
  owner, repo: repoName,
  tree: treeEntries,
  base_tree: parentCommit.tree.sha,  // <-- causes 404
});

// After (works with both OAuth and PAT):
const { data: tree } = await this.#octokit.git.createTree({
  owner, repo: repoName,
  tree: treeEntries,
  // no base_tree
});

Why auto_init is still needed

GitHub's git data endpoints (POST /repos/{owner}/{repo}/git/blobs, /git/trees, etc.) return 409 Git Repository is empty on repos with zero commits. There is no way around this, some form of initialization is required. auto_init: true is the standard approach. The extra initial commit becomes a parent in the history, which is harmless.

Root cause

Unclear why base_tree causes a 404 specifically with OAuth tokens. The tree SHA is valid (returned by getCommit moments earlier). This may be a GitHub API inconsistency in how OAuth app tokens resolve git object references on newly created repos.

@matejvasek

Copy link
Copy Markdown
Collaborator

Disconnect does not refresh/clean the functions list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants