Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a78f0e0ce |
3 changed files with 291 additions and 83 deletions
166
BINARYBEACHIO.md
Normal file
166
BINARYBEACHIO.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# bb-plane-fork — binarybeachio customizations of Plane
|
||||
|
||||
This file is the canonical contract between this fork and the binarybeachio platform repo. It exists so anyone (or any agent) on a fresh session can answer "what's customized, why, and how do I refresh from upstream" without reading code.
|
||||
|
||||
**Fork repo convention** (template — same shape for every Path B fork in binarybeachio):
|
||||
|
||||
```
|
||||
upstream remote → original project on github.com (read-only, merge-source)
|
||||
origin remote → git.binarybeach.io/binarybeach/bb-<name>-fork (where we push)
|
||||
github mirror → github.com/binarybeachllc/bb-<name>-fork (push-mirror, off-site backup)
|
||||
|
||||
upstream branch — clean mirror of upstream's default branch, never modified
|
||||
main branch — our customizations on top of latest upstream tag we've integrated
|
||||
update/<v> — short-lived integration branch when pulling a new upstream version
|
||||
```
|
||||
|
||||
`git log main..upstream` = "upstream changes I haven't pulled in"
|
||||
`git log upstream..main` = "binarybeachio's customizations"
|
||||
|
||||
---
|
||||
|
||||
## Upstream
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Project | Plane (open-source project management) |
|
||||
| Upstream repo | https://github.com/makeplane/plane |
|
||||
| Upstream default branch | `preview` |
|
||||
| Currently integrated upstream version | **v1.3.0** (release commit `cf696d2`) |
|
||||
| License | AGPL-3.0-only (we MUST publish source of any deployed customizations — push-mirror to GitHub satisfies this) |
|
||||
|
||||
## Why we forked
|
||||
|
||||
Plane's first-party OIDC support is gated behind the **Pro/Business commercial edition** (Pro tier minimum 25 users = $338+/mo). The community edition's `/god-mode/authentication/oidc` page is a frontend stub — the backend handler returns 404 (verified 2026-04-29 against pm.binarybeach.io).
|
||||
|
||||
We don't want to pay $338/mo for a single binarybeachio operator's SSO. We DO want every self-hosted service to authenticate users via the same Zitadel IdP (the break-glass admin convention from `binarybeachio/docs/architecture/self-hosting-infrastructure.md` §6.1 demands it).
|
||||
|
||||
Plane's backend has working **community-edition** GitHub OAuth (`/auth/github/...`). We repurpose that flow to point at Zitadel by env-driving the four GitHub URL constants and switching the userinfo claim mapping to OIDC standard. This is described in detail in `apps/api/plane/authentication/provider/oauth/github.py`'s header comment.
|
||||
|
||||
## What's customized (the inventory — keep current)
|
||||
|
||||
Touch surface is intentionally minimal. **Two files**, both narrowly scoped, designed to minimize merge conflict probability on every Plane upgrade.
|
||||
|
||||
| File | Change | Risk on upgrade |
|
||||
|---|---|---|
|
||||
| `apps/api/plane/authentication/provider/oauth/github.py` | Repurposed entire file: env-drive endpoint URLs (default to `$ZITADEL_DOMAIN`'s OIDC endpoints, fall back to GitHub when `ZITADEL_DOMAIN` unset). Switch claim mapping to OIDC standard. Drop `__get_email` (OIDC userinfo includes email). Fix upstream's `expires_in` epoch-vs-duration bug. Drop `is_user_in_organization` (Zitadel handles authz). | **Medium.** This file rarely changes upstream. If Plane refactors the OauthAdapter base class signatures, our patched constructor must follow. |
|
||||
| `apps/web/core/hooks/oauth/core.tsx` | Cosmetic: rename "GitHub" button text to "binarybeach.io". Backend ID/route/icon path unchanged. | **Low.** Pure cosmetic; rebases trivially. |
|
||||
|
||||
Files **not** changed (deliberately):
|
||||
- `apps/api/plane/authentication/views/app/github.py` — view layer, unchanged. Routes still `/auth/github/`.
|
||||
- `apps/api/plane/authentication/views/space/github.py` — public-share OAuth, unchanged.
|
||||
- `apps/api/plane/authentication/urls.py` — URL routing unchanged.
|
||||
- `apps/admin/...` — god-mode UI still says "GitHub" provider; only the operator (us) sees it, not worth the patch surface.
|
||||
- `apps/space/...` — public sharing site OAuth, not a priority for v1.
|
||||
|
||||
## Required runtime config
|
||||
|
||||
Set these env vars on the patched `plane-backend` container (binarybeachio sets them in `infrastructure/plane/.env`):
|
||||
|
||||
```bash
|
||||
# Pin our Zitadel host — this enables the OIDC code path. Without it, the
|
||||
# patched provider falls back to vanilla GitHub OAuth (deliberate).
|
||||
ZITADEL_DOMAIN=auth.binarybeach.io
|
||||
|
||||
# Optional explicit overrides if endpoints differ from Zitadel defaults.
|
||||
# Defaults derive from ZITADEL_DOMAIN: /oauth/v2/{authorize,token}, /oidc/v1/userinfo.
|
||||
# OIDC_AUTH_URL=
|
||||
# OIDC_TOKEN_URL=
|
||||
# OIDC_USERINFO_URL=
|
||||
|
||||
# Existing Plane env vars (kept names — backend still calls them GITHUB_*)
|
||||
GITHUB_CLIENT_ID=<zitadel-app-client-id>
|
||||
GITHUB_CLIENT_SECRET=<zitadel-app-client-secret>
|
||||
```
|
||||
|
||||
And in Plane's god-mode admin UI (`/god-mode/authentication/github`):
|
||||
- Toggle GitHub OAuth ON
|
||||
- Paste the same client_id/secret (god-mode DB rows shadow env vars at runtime — both must agree)
|
||||
|
||||
In Zitadel:
|
||||
- Create OIDC Web application
|
||||
- Redirect URI: `https://pm.binarybeach.io/auth/github/callback/` (production) and `http://localhost/auth/github/callback/` (local test)
|
||||
- Auth method: client_secret_post (Plane sends creds in body)
|
||||
- Grant types: Authorization Code + Refresh Token
|
||||
- Response types: code
|
||||
|
||||
## Refresh from upstream — the procedure
|
||||
|
||||
When a new Plane release lands and we want to integrate:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
# Sync the upstream mirror branch (never touched by us)
|
||||
git switch upstream
|
||||
git reset --hard upstream/preview # or @v1.4.0 if we track tags
|
||||
git push origin upstream
|
||||
|
||||
# Integration branch
|
||||
git switch main
|
||||
git switch -c update/v1.4.0
|
||||
git merge upstream # resolve any conflicts (likely in github.py)
|
||||
# Run all tests, hand-test the OIDC flow against staging Zitadel
|
||||
# Once happy:
|
||||
git switch main
|
||||
git merge --ff-only update/v1.4.0
|
||||
git branch -d update/v1.4.0
|
||||
git push origin main
|
||||
|
||||
# Then on laptop: rebuild + tag + push images (see "Build" below)
|
||||
# Then in binarybeachio repo: bump tag in infrastructure/plane/docker-compose.yml
|
||||
# Then: py infrastructure/_shared/bootstrap.py to trigger the Coolify deploy
|
||||
```
|
||||
|
||||
## Build — which images to rebuild and how
|
||||
|
||||
Per binarybeachio architecture doc §7.4 ("only rebuild what we touched"), this fork only requires rebuilding **two of the six** Plane images:
|
||||
|
||||
| Image | Customized? | Source | Build target |
|
||||
|---|---|---|---|
|
||||
| `plane-backend` | YES | `apps/api/Dockerfile.api` | `git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.<n>` |
|
||||
| `plane-frontend` (aka web) | YES | `apps/web/Dockerfile.web` | `git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.<n>` |
|
||||
| `plane-space` | no | upstream `makeplane/plane-space:v1.3.0` | (no rebuild) |
|
||||
| `plane-admin` | no | upstream `makeplane/plane-admin:v1.3.0` | (no rebuild) |
|
||||
| `plane-live` | no | upstream `makeplane/plane-live:v1.3.0` | (no rebuild) |
|
||||
| `plane-proxy` | no | upstream `makeplane/plane-proxy:v1.3.0` | (no rebuild) |
|
||||
|
||||
The binarybeachio compose file at `infrastructure/plane/docker-compose.yml` mixes our patched images with upstream-vanilla images for the four we don't touch.
|
||||
|
||||
Tag scheme per architecture §6 #7: `<upstream-version>-mine.<n>`. Push immutable tag + `:latest`:
|
||||
|
||||
```bash
|
||||
# from C:\Users\maxwe\GitHubRepos\bb-plane-fork
|
||||
docker build -t git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.1 \
|
||||
-t git.binarybeach.io/binarybeach/plane-backend:latest \
|
||||
-f apps/api/Dockerfile.api .
|
||||
docker push git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.1
|
||||
docker push git.binarybeach.io/binarybeach/plane-backend:latest
|
||||
|
||||
docker build -t git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.1 \
|
||||
-t git.binarybeach.io/binarybeach/plane-frontend:latest \
|
||||
-f apps/web/Dockerfile.web .
|
||||
docker push git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.1
|
||||
docker push git.binarybeach.io/binarybeach/plane-frontend:latest
|
||||
```
|
||||
|
||||
`mine.<n>` resets to `mine.1` on every upstream version bump; increments per local rebuild within the same upstream version.
|
||||
|
||||
## License compliance
|
||||
|
||||
Plane is AGPL-3.0-only. The license requires us to provide the source of any modified version we deploy or offer over a network. Our compliance:
|
||||
|
||||
1. **Forgejo source** — `git.binarybeach.io/binarybeach/bb-plane-fork` is a public-readable repository (Forgejo `DEFAULT_PRIVATE=public`).
|
||||
2. **GitHub mirror** — push-mirror to `github.com/binarybeachllc/bb-plane-fork` provides off-site backup AND a publicly-discoverable source location even if Forgejo is unreachable.
|
||||
3. **In-product source link** — TODO: add a footer link in our customized `apps/web` to https://git.binarybeach.io/binarybeach/bb-plane-fork. AGPL §13 requires "prominent" notice to network users; a footer suffices.
|
||||
|
||||
The TODO in #3 is tracked in the parent binarybeachio repo's compliance log when we get there. Not a v1 blocker — Plane already includes upstream license notices and our changes preserve them.
|
||||
|
||||
## Test plan (manual, until we have CI)
|
||||
|
||||
1. **Local build smoke**: both images build cleanly with current Dockerfiles.
|
||||
2. **Local stack**: `docker compose -f docker-compose-local.yml up -d` (using patched images), pointed at hosted Zitadel.
|
||||
3. **OIDC flow**: visit `http://localhost`, click "Continue with binarybeach.io", redirected to `auth.binarybeach.io`, log in as Zitadel user, redirected back, account auto-provisioned in Plane, signed in.
|
||||
4. **New-user flow**: sign in with a Zitadel user that doesn't yet exist in Plane → Plane auto-creates the account.
|
||||
5. **Re-login**: sign out, sign in again with same Zitadel user → matched by email, same Plane user.
|
||||
6. **Fallback**: unset `ZITADEL_DOMAIN` env var, restart backend, try GitHub OAuth flow with real GitHub creds → should still work (regression check that we didn't break upstream behavior).
|
||||
7. **Production deploy**: bump tag in binarybeachio `infrastructure/plane/docker-compose.yml` → `py infrastructure/_shared/bootstrap.py` → verify on `pm.binarybeach.io`.
|
||||
|
|
@ -1,14 +1,41 @@
|
|||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
#
|
||||
# binarybeachio fork — see BINARYBEACHIO.md at repo root.
|
||||
# This file is patched to repurpose Plane's "GitHub" OAuth provider as a
|
||||
# generic OIDC provider, so we can point /auth/github/ at our self-hosted
|
||||
# Zitadel instance without paying for Plane Pro/Business edition's first-party
|
||||
# OIDC support.
|
||||
#
|
||||
# Touch points kept stable to minimize merge conflicts on Plane upgrades:
|
||||
# - class name `GitHubOAuthProvider` (callers import it by name)
|
||||
# - `provider = "github"` (DB rows keyed on this string)
|
||||
# - env var names `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`
|
||||
# - URL routes `/auth/github/...` (frontend hardcodes these)
|
||||
#
|
||||
# What changed:
|
||||
# - `auth_url` / `token_url` / `userinfo_url` are now read from env, default
|
||||
# to the Zitadel instance at $ZITADEL_DOMAIN. If `ZITADEL_DOMAIN` is unset
|
||||
# the original GitHub URLs apply, so vanilla GitHub OAuth still works as a
|
||||
# fallback (lets us re-test against upstream behavior without reverting).
|
||||
# - Scope flipped from "read:user user:email" to "openid email profile" when
|
||||
# pointed at Zitadel (or any OIDC IdP).
|
||||
# - `__get_email` removed — standard OIDC userinfo includes `email` directly.
|
||||
# - User claim mapping switched to OIDC standard: sub, name, given_name,
|
||||
# family_name, email, picture.
|
||||
# - Fixed upstream bug where `expires_in` (a duration in seconds) was being
|
||||
# passed to datetime.fromtimestamp() (which expects an epoch timestamp).
|
||||
# - Dropped `is_user_in_organization` — Zitadel handles authorization itself
|
||||
# via project grants/roles. The `GITHUB_ORGANIZATION_ID` env stays read
|
||||
# (no-op) to avoid breaking deployments that have it set.
|
||||
|
||||
# Python imports
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from django.utils import timezone
|
||||
|
||||
from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
|
|
@ -20,15 +47,30 @@ from plane.authentication.adapter.oauth import OauthAdapter
|
|||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
def _zitadel_default(path: str) -> str | None:
|
||||
"""Build a Zitadel endpoint URL from $ZITADEL_DOMAIN if set."""
|
||||
domain = os.environ.get("ZITADEL_DOMAIN")
|
||||
return f"https://{domain}{path}" if domain else None
|
||||
|
||||
|
||||
class GitHubOAuthProvider(OauthAdapter):
|
||||
token_url = "https://github.com/login/oauth/access_token"
|
||||
userinfo_url = "https://api.github.com/user"
|
||||
org_membership_url = "https://api.github.com/orgs"
|
||||
# Endpoint URLs — env-driven. Defaults derived from $ZITADEL_DOMAIN if set,
|
||||
# falling back to GitHub.com to preserve upstream behavior when unset.
|
||||
token_url = os.environ.get("OIDC_TOKEN_URL") or (
|
||||
_zitadel_default("/oauth/v2/token") or "https://github.com/login/oauth/access_token"
|
||||
)
|
||||
userinfo_url = os.environ.get("OIDC_USERINFO_URL") or (
|
||||
_zitadel_default("/oidc/v1/userinfo") or "https://api.github.com/user"
|
||||
)
|
||||
_auth_url_base = os.environ.get("OIDC_AUTH_URL") or (
|
||||
_zitadel_default("/oauth/v2/authorize") or "https://github.com/login/oauth/authorize"
|
||||
)
|
||||
|
||||
provider = "github"
|
||||
scope = "read:user user:email"
|
||||
|
||||
organization_scope = "read:org"
|
||||
# Scopes — OIDC standard when ZITADEL_DOMAIN is set; GitHub-flavored otherwise
|
||||
# to match unpatched upstream behavior for fallback testing.
|
||||
scope = "openid email profile" if os.environ.get("ZITADEL_DOMAIN") else "read:user user:email"
|
||||
|
||||
def __init__(self, request, code=None, state=None, callback=None):
|
||||
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value([
|
||||
|
|
@ -54,11 +96,13 @@ class GitHubOAuthProvider(OauthAdapter):
|
|||
|
||||
client_id = GITHUB_CLIENT_ID
|
||||
client_secret = GITHUB_CLIENT_SECRET
|
||||
# Read but unused — kept for API compatibility with deployments that
|
||||
# had this set under upstream Plane. Authorization in our setup is
|
||||
# handled by Zitadel project grants, not client-side org membership.
|
||||
self.organization_id = GITHUB_ORGANIZATION_ID
|
||||
|
||||
if self.organization_id:
|
||||
self.scope += f" {self.organization_scope}"
|
||||
|
||||
# Build redirect_uri — must match what's registered with the IdP.
|
||||
# Plane's frontend hardcodes /auth/github/callback/ so we keep that path.
|
||||
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
|
|
@ -66,7 +110,10 @@ class GitHubOAuthProvider(OauthAdapter):
|
|||
"scope": self.scope,
|
||||
"state": state,
|
||||
}
|
||||
auth_url = f"https://github.com/login/oauth/authorize?{urlencode(url_params)}"
|
||||
# OIDC requires response_type=code; GitHub OAuth tolerates it.
|
||||
if os.environ.get("ZITADEL_DOMAIN"):
|
||||
url_params["response_type"] = "code"
|
||||
auth_url = f"{self._auth_url_base}?{urlencode(url_params)}"
|
||||
super().__init__(
|
||||
request,
|
||||
self.provider,
|
||||
|
|
@ -83,93 +130,84 @@ class GitHubOAuthProvider(OauthAdapter):
|
|||
|
||||
def set_token_data(self):
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"code": self.code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
}
|
||||
token_response = self.get_user_token(data=data, headers={"Accept": "application/json"})
|
||||
|
||||
# Fix upstream bug: `expires_in` is a duration (seconds) per RFC 6749,
|
||||
# not an epoch timestamp. Compute absolute expiry correctly.
|
||||
expires_in = token_response.get("expires_in")
|
||||
access_token_expired_at = (
|
||||
timezone.now() + timedelta(seconds=int(expires_in)) if expires_in else None
|
||||
)
|
||||
# `refresh_token_expired_at` is non-standard; some IdPs return it as
|
||||
# absolute, some as duration. Zitadel doesn't return it at all. Keep the
|
||||
# original interpretation as-epoch for backward-compat with upstream.
|
||||
refresh_expired_raw = token_response.get("refresh_token_expired_at")
|
||||
if refresh_expired_raw:
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
refresh_token_expired_at = datetime.fromtimestamp(refresh_expired_raw, tz=pytz.utc)
|
||||
else:
|
||||
refresh_token_expired_at = None
|
||||
|
||||
super().set_token_data({
|
||||
"access_token": token_response.get("access_token"),
|
||||
"refresh_token": token_response.get("refresh_token", None),
|
||||
"access_token_expired_at": (
|
||||
datetime.fromtimestamp(token_response.get("expires_in"), tz=pytz.utc)
|
||||
if token_response.get("expires_in")
|
||||
else None
|
||||
),
|
||||
"refresh_token_expired_at": (
|
||||
datetime.fromtimestamp(token_response.get("refresh_token_expired_at"), tz=pytz.utc)
|
||||
if token_response.get("refresh_token_expired_at")
|
||||
else None
|
||||
),
|
||||
"access_token_expired_at": access_token_expired_at,
|
||||
"refresh_token_expired_at": refresh_token_expired_at,
|
||||
"id_token": token_response.get("id_token", ""),
|
||||
})
|
||||
|
||||
def __get_email(self, headers):
|
||||
try:
|
||||
# Github does not provide email in user response
|
||||
emails_url = "https://api.github.com/user/emails"
|
||||
emails_response = requests.get(emails_url, headers=headers).json()
|
||||
# Ensure the response is a list before iterating
|
||||
if not isinstance(emails_response, list):
|
||||
self.logger.error("Unexpected response format from GitHub emails API")
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
email = next((email["email"] for email in emails_response if email["primary"]), None)
|
||||
if not email:
|
||||
self.logger.error("No primary email found for user")
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
return email
|
||||
except requests.RequestException:
|
||||
self.logger.warning(
|
||||
"Error getting email from GitHub",
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
|
||||
def is_user_in_organization(self, github_username):
|
||||
headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"}
|
||||
response = requests.get(
|
||||
f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}",
|
||||
headers=headers,
|
||||
)
|
||||
return response.status_code == 200 # 200 means the user is a member
|
||||
|
||||
def set_user_data(self):
|
||||
user_info_response = self.get_user_response()
|
||||
|
||||
# Claim mapping. When ZITADEL_DOMAIN is set, use OIDC standard claims;
|
||||
# otherwise fall back to GitHub's quirky shape (no email in userinfo,
|
||||
# `name` instead of `given_name`/`family_name`).
|
||||
if os.environ.get("ZITADEL_DOMAIN"):
|
||||
email = user_info_response.get("email")
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
super().set_user_data({
|
||||
"email": email,
|
||||
"user": {
|
||||
"provider_id": user_info_response.get("sub"),
|
||||
"email": email,
|
||||
"avatar": user_info_response.get("picture"),
|
||||
"first_name": user_info_response.get("given_name") or user_info_response.get("name", "").split(" ", 1)[0],
|
||||
"last_name": user_info_response.get("family_name") or (user_info_response.get("name", "").split(" ", 1)[1] if " " in user_info_response.get("name", "") else ""),
|
||||
"is_password_autoset": True,
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
# Fallback: vanilla GitHub OAuth — keep upstream behavior. Email comes
|
||||
# from a separate /user/emails call.
|
||||
import requests
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token_data.get('access_token')}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
if self.organization_id:
|
||||
if not self.is_user_in_organization(user_info_response.get("login")):
|
||||
self.logger.warning(
|
||||
"User is not in organization",
|
||||
extra={
|
||||
"organization_id": self.organization_id,
|
||||
"user_login": user_info_response.get("login"),
|
||||
},
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_USER_NOT_IN_ORG"],
|
||||
error_message="GITHUB_USER_NOT_IN_ORG",
|
||||
)
|
||||
|
||||
email = self.__get_email(headers=headers)
|
||||
self.logger.debug(
|
||||
"Email found",
|
||||
extra={
|
||||
"email": email,
|
||||
},
|
||||
)
|
||||
emails_response = requests.get("https://api.github.com/user/emails", headers=headers).json()
|
||||
if not isinstance(emails_response, list):
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
email = next((e["email"] for e in emails_response if e["primary"]), None)
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_OAUTH_PROVIDER_ERROR"],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
super().set_user_data({
|
||||
"email": email,
|
||||
"user": {
|
||||
|
|
|
|||
|
|
@ -46,14 +46,18 @@ export const useCoreOAuthConfig = (oauthActionText: string): TOAuthConfigs => {
|
|||
enabled: config?.is_google_enabled,
|
||||
},
|
||||
{
|
||||
// binarybeachio fork — this OAuth slot is repurposed as our Zitadel SSO
|
||||
// entry point (the backend's GitHubOAuthProvider was patched to point at
|
||||
// Zitadel — see provider/oauth/github.py). Branding is rebranded here;
|
||||
// backend identifiers (route, env vars, DB provider key) stay "github".
|
||||
id: "github",
|
||||
text: `${oauthActionText} with GitHub`,
|
||||
text: `${oauthActionText} with binarybeach.io`,
|
||||
icon: (
|
||||
<img
|
||||
src={resolvedTheme === "dark" ? GithubDarkLogo : GithubLightLogo}
|
||||
height={18}
|
||||
width={18}
|
||||
alt="GitHub Logo"
|
||||
alt="binarybeach.io SSO"
|
||||
/>
|
||||
),
|
||||
onClick: () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue