This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 50f664e49 feat(auth): add OIDC allowlist restrictions (#8984)
50f664e49 is described below

commit 50f664e495d9b1c752fbc237af9c5d8b4cfeae76
Author: Mauricio Apuril <[email protected]>
AuthorDate: Thu Jul 9 22:37:28 2026 -0300

    feat(auth): add OIDC allowlist restrictions (#8984)
---
 backend/helpers/oidchelper/authorization.go      |  42 ++++++++
 backend/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(+)

diff --git a/backend/helpers/oidchelper/authorization.go 
b/backend/helpers/oidchelper/authorization.go
new file mode 100644
index 000000000..c740666a2
--- /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 000000000..e73a32e92
--- /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: "[email protected]",
+                       want:  true,
+               },
+               {
+                       name: "allowed email",
+                       cfg: Config{
+                               AllowEmails: map[string]struct{}{
+                                       "[email protected]": {},
+                               },
+                       },
+                       email: "[email protected]",
+                       want:  true,
+               },
+               {
+                       name: "blocked email",
+                       cfg: Config{
+                               AllowEmails: map[string]struct{}{
+                                       "[email protected]": {},
+                               },
+                       },
+                       email: "[email protected]",
+                       want:  false,
+               },
+               {
+                       name: "allowed domain",
+                       cfg: Config{
+                               AllowDomains: map[string]struct{}{
+                                       "example.com": {},
+                               },
+                       },
+                       email: "[email protected]",
+                       want:  true,
+               },
+               {
+                       name: "blocked domain",
+                       cfg: Config{
+                               AllowDomains: map[string]struct{}{
+                                       "example.com": {},
+                               },
+                       },
+                       email: "[email protected]",
+                       want:  false,
+               },
+               {
+                       name: "email case insensitive",
+                       cfg: Config{
+                               AllowEmails: map[string]struct{}{
+                                       "[email protected]": {},
+                               },
+                       },
+                       email: "[email protected]",
+                       want:  true,
+               },
+               {
+                       name: "domain case insensitive",
+                       cfg: Config{
+                               AllowDomains: map[string]struct{}{
+                                       "example.com": {},
+                               },
+                       },
+                       email: "[email protected]",
+                       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 a23606df1..9d1b8ae47 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 ea7029a38..aead556a5 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 7d6991550..04e0ab0a5 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=

Reply via email to