From 51808cfb13dbf11f7910cc8b9fa994c134615b66 Mon Sep 17 00:00:00 2001 From: Mauricio <117807067+MauricioApu@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:14:33 -0300 Subject: [PATCH] feat(auth): add OIDC allowlist restrictions --- backend/helpers/oidchelper/authorization.go | 42 +++++++ .../helpers/oidchelper/authorization_test.go | 119 ++++++++++++++++++ backend/helpers/oidchelper/config.go | 24 ++++ backend/server/api/auth/auth.go | 6 + env.example | 4 + 5 files changed, 195 insertions(+) create mode 100644 backend/helpers/oidchelper/authorization.go create mode 100644 backend/helpers/oidchelper/authorization_test.go diff --git a/backend/helpers/oidchelper/authorization.go b/backend/helpers/oidchelper/authorization.go new file mode 100644 index 00000000000..c740666a275 --- /dev/null +++ b/backend/helpers/oidchelper/authorization.go @@ -0,0 +1,42 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oidchelper + +import "strings" + +func (c *Config) IsUserAllowed(email string) bool { + if len(c.AllowEmails) == 0 && + len(c.AllowDomains) == 0 { + return true + } + + email = strings.ToLower(strings.TrimSpace(email)) + + if _, ok := c.AllowEmails[email]; ok { + return true + } + + _, domain, ok := strings.Cut(email, "@") + if ok { + if _, ok := c.AllowDomains[domain]; ok { + return true + } + } + + return false +} diff --git a/backend/helpers/oidchelper/authorization_test.go b/backend/helpers/oidchelper/authorization_test.go new file mode 100644 index 00000000000..e73a32e92d3 --- /dev/null +++ b/backend/helpers/oidchelper/authorization_test.go @@ -0,0 +1,119 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oidchelper + +import "testing" + +func TestIsUserAllowed(t *testing.T) { + cases := []struct { + name string + cfg Config + email string + want bool + }{ + { + name: "no restrictions", + cfg: Config{}, + email: "user@example.com", + want: true, + }, + { + name: "allowed email", + cfg: Config{ + AllowEmails: map[string]struct{}{ + "user@example.com": {}, + }, + }, + email: "user@example.com", + want: true, + }, + { + name: "blocked email", + cfg: Config{ + AllowEmails: map[string]struct{}{ + "user@example.com": {}, + }, + }, + email: "other@example.com", + want: false, + }, + { + name: "allowed domain", + cfg: Config{ + AllowDomains: map[string]struct{}{ + "example.com": {}, + }, + }, + email: "user@example.com", + want: true, + }, + { + name: "blocked domain", + cfg: Config{ + AllowDomains: map[string]struct{}{ + "example.com": {}, + }, + }, + email: "user@other.com", + want: false, + }, + { + name: "email case insensitive", + cfg: Config{ + AllowEmails: map[string]struct{}{ + "user@example.com": {}, + }, + }, + email: "USER@example.com", + want: true, + }, + { + name: "domain case insensitive", + cfg: Config{ + AllowDomains: map[string]struct{}{ + "example.com": {}, + }, + }, + email: "user@EXAMPLE.COM", + want: true, + }, + { + name: "invalid email", + cfg: Config{ + AllowDomains: map[string]struct{}{ + "example.com": {}, + }, + }, + email: "not-an-email", + want: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.cfg.IsUserAllowed(tc.email); got != tc.want { + t.Errorf( + "IsUserAllowed(%q) = %v, want %v", + tc.email, + got, + tc.want, + ) + } + }) + } +} diff --git a/backend/helpers/oidchelper/config.go b/backend/helpers/oidchelper/config.go index a23606df1d4..9d1b8ae47c2 100644 --- a/backend/helpers/oidchelper/config.go +++ b/backend/helpers/oidchelper/config.go @@ -67,6 +67,11 @@ type Config struct { Providers map[string]*ProviderConfig LogoutRedirect bool + // Optional OIDC authorization restrictions. + // Empty means no restriction. + AllowEmails map[string]struct{} + AllowDomains map[string]struct{} + SessionSecret []byte SessionTTL time.Duration @@ -136,6 +141,8 @@ func LoadConfig(basicRes context.BasicRes) (*Config, error) { SessionTTL: ttl, CookieDomain: strings.TrimSpace(cfg.GetString("COOKIE_DOMAIN")), CookieSecure: cookieSecure, + AllowEmails: parseStringSet(cfg.GetString("OIDC_ALLOW_EMAILS")), + AllowDomains: parseStringSet(cfg.GetString("OIDC_ALLOW_DOMAINS")), } if !out.OIDCEnabled { @@ -205,6 +212,23 @@ func parseProviderNames(raw string) []string { seen[n] = struct{}{} out = append(out, n) } + + return out +} + +func parseStringSet(raw string) map[string]struct{} { + out := make(map[string]struct{}) + + for _, v := range strings.Split(raw, ",") { + v = strings.ToLower(strings.TrimSpace(v)) + + if v == "" { + continue + } + + out[v] = struct{}{} + } + return out } diff --git a/backend/server/api/auth/auth.go b/backend/server/api/auth/auth.go index ea7029a3824..aead556a5bc 100644 --- a/backend/server/api/auth/auth.go +++ b/backend/server/api/auth/auth.go @@ -308,6 +308,12 @@ func (s *Service) Callback(c *gin.Context) { fail(c, http.StatusBadGateway, "extract claims", err) return } + + if !s.cfg.IsUserAllowed(email) { + fail(c, http.StatusForbidden, "user is not allowed", nil) + return + } + jti := uuid.NewString() jwt, expiresAt, err := oidchelper.IssueSession(s.cfg, jti, state.Provider, sub, email, name) if err != nil { diff --git a/env.example b/env.example index 7d69915505a..04e0ab0a5ed 100755 --- a/env.example +++ b/env.example @@ -162,3 +162,7 @@ SESSION_TTL=8h COOKIE_DOMAIN= # Set to false ONLY for local HTTP development. COOKIE_SECURE=true + +# Restrict OIDC logins to specific users or domains (comma-separated, case-insensitive) +OIDC_ALLOW_EMAILS= +OIDC_ALLOW_DOMAINS=