Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
42918cb
feat(auth): load and validate OpenID Connect settings
TartanLeGrand Sep 28, 2026
abf00e1
test(auth): add an in-process OpenID Connect provider for tests
TartanLeGrand Sep 28, 2026
5fc02d8
feat(auth): add the OpenID Connect provider client
TartanLeGrand Sep 28, 2026
2875087
feat(auth): encrypt the OpenID Connect login transaction cookie
TartanLeGrand Sep 29, 2026
2ebdf89
fix(auth): bound the OpenID Connect transaction cookie size
TartanLeGrand Sep 29, 2026
51d4dd0
feat(auth): resolve and provision OpenID Connect users
TartanLeGrand Sep 29, 2026
db04abe
fix(auth): harden OpenID Connect user resolution
TartanLeGrand Sep 29, 2026
9077d51
feat(auth): sync team membership from OpenID Connect groups
TartanLeGrand Sep 29, 2026
4884f52
fix(auth): refuse OpenID Connect logins with a missing groups claim
TartanLeGrand Sep 29, 2026
8cea78c
fix(auth): add every matching OpenID Connect team on sync
TartanLeGrand Sep 29, 2026
31a7481
feat(auth): OpenID Connect login and callback routes
TartanLeGrand Sep 29, 2026
0bf1f7a
fix(auth): check the OpenID Connect groups claim before any write
TartanLeGrand Sep 29, 2026
710baa4
test(auth): cover the OpenID Connect security properties end to end
TartanLeGrand Sep 29, 2026
3fe4e48
docs(auth): document OpenID Connect login and identity provider setup
TartanLeGrand Sep 29, 2026
1660f39
fix(auth): allow OpenID Connect users without groups when the claim i…
TartanLeGrand Sep 29, 2026
e3f90d0
docs: document the OpenID Connect groups claim rule
TartanLeGrand Sep 29, 2026
4d93430
fix(auth): never remove teams when the OpenID Connect groups claim is…
TartanLeGrand Sep 29, 2026
f423a00
fix(auth): warn when an OpenID Connect login omits the groups claim
TartanLeGrand Sep 29, 2026
49b34c6
fix(auth): treat an unexpected groups claim type as absent
TartanLeGrand Sep 29, 2026
3720568
docs(auth): correct the OpenID Connect error codes and account notes
TartanLeGrand Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,14 @@ AUTH_COOKIE_SECURE=false
# Trust the last X-Forwarded-For entry for login rate limiting, only behind a
# reverse proxy you control that appends the peer address to the header.
AUTH_TRUST_PROXY=false
# OpenID Connect login (see docs/AUTHENTICATION.md). Leave AUTH_OIDC_ISSUER empty to disable it.
# AUTH_PUBLIC_URL is required when it is set.
AUTH_OIDC_ISSUER=
AUTH_OIDC_CLIENT_ID=
AUTH_OIDC_CLIENT_SECRET=
# AUTH_OIDC_SCOPES=openid profile email
# AUTH_OIDC_GROUPS_CLAIM=groups
# AUTH_OIDC_USERNAME_CLAIM=preferred_username
# AUTH_OIDC_USER_PROVISIONING=true
# AUTH_OIDC_TEAM_SYNC=true
# AUTH_OIDC_BUTTON_LABEL=Single Sign-On
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ npm run dev
- [🚀 Installation Guide](./docs/INSTALLATION.md) - Complete installation instructions
- [⚙️ Configuration Guide](./docs/CONFIGURATION.md) - Environment variables and settings
- [🔧 Development Guide](./docs/DEVELOPMENT.md) - Set up development environment
- [🔐 Authentication](./docs/AUTHENTICATION.md) - Users, teams, permissions and API keys
- [🔐 Authentication](./docs/AUTHENTICATION.md) - Users, teams, permissions, API keys and single sign-on

### User Guides
- [📖 User Guide](./docs/USER_GUIDE.md) - How to use Tracker
Expand Down
20 changes: 20 additions & 0 deletions cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
lock "github.com/bananaops/tracker/generated/proto/lock/v1alpha1"
"github.com/bananaops/tracker/internal/auth"
"github.com/bananaops/tracker/internal/auth/identity"
"github.com/bananaops/tracker/internal/auth/sso"
store "github.com/bananaops/tracker/internal/stores"
"github.com/bananaops/tracker/server"
"github.com/go-openapi/runtime/middleware"
Expand Down Expand Up @@ -149,6 +150,25 @@ var serv = &cobra.Command{
// Cookie based auth endpoints (login, logout, password change)
server.NewAuthHTTP(userStore, sessions, authCfg).Register(mux)

// OpenID Connect login, only when AUTH_OIDC_ISSUER is set. Discovery is
// lazy: an unreachable identity provider must not keep Tracker from
// starting, the local admin account stays the way in.
if authCfg.OIDC.Enabled() {
codec, err := sso.NewTransactionCodec(sessionSecret)
if err != nil {
log.Fatalf("cannot create the OIDC transaction codec: %v", err)
}
provider := sso.NewOIDCProvider(authCfg.OIDC, authCfg.OIDCRedirectURL())
go func() {
// The error is not logged: go-oidc embeds the raw response body.
if err := provider.Discover(context.Background()); err != nil {
slog.Warn("OIDC discovery failed at startup, it is retried on the next login", "issuer", authCfg.OIDC.Issuer, "reason", "provider_unavailable")
}
}()
server.NewOIDCHTTP(userStore, teamStore, sessions, provider, codec, authCfg).Register(mux)
slog.Info("OIDC login enabled", "oidc", authCfg.OIDC, "redirect_uri", authCfg.OIDCRedirectURL())
}

// Register Homer proxy endpoint
server.RegisterHomerHandler(mux, os.Getenv("HOMER_URL"))

Expand Down
302 changes: 298 additions & 4 deletions docs/AUTHENTICATION.md

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,22 @@ BUY_ME_COFFEE_URL=https://www.buymeacoffee.com/yourname
| `AUTH_ADMIN_PASSWORD` | generated | Password of the initial `admin` account. Only used when no user exists yet. When unset, a random password is printed once in the logs. |
| `AUTH_SESSION_SECRET` | persisted in MongoDB | Base64 secret (32 bytes minimum) signing session cookies. Set it explicitly when running several replicas without a shared database secret. |
| `AUTH_SESSION_TTL` | `12h` | Session lifetime. |
| `AUTH_PUBLIC_URL` | - | Public URL of the UI. An `https` URL makes cookies `Secure`. |
| `AUTH_PUBLIC_URL` | - | Public URL of the UI. An `https` URL makes cookies `Secure`. Required with OIDC, where it is the base of the redirect URI (`scheme://host[:port]`, no path). |
| `AUTH_COOKIE_SECURE` | `false` | Force the `Secure` flag on cookies. |
| `AUTH_TRUST_PROXY` | `false` | Use the last entry of `X-Forwarded-For` as client IP for login rate limiting, and `X-Forwarded-Proto` to decide the request scheme. Only enable it behind a reverse proxy that appends the peer address to the header. |
| `AUTH_OIDC_ISSUER` | - | OpenID Connect issuer URL. Setting it enables single sign-on. `https` only (`http` for loopback). Must match the token `iss` exactly. |
| `AUTH_OIDC_CLIENT_ID` | - | OIDC client ID. Required with an issuer. |
| `AUTH_OIDC_CLIENT_SECRET` | - | OIDC client secret. Required with an issuer. Keep it in a secret store. |
| `AUTH_OIDC_SCOPES` | `openid profile email` | Requested scopes, space or comma separated. |
| `AUTH_OIDC_GROUPS_CLAIM` | `groups` | `id_token` claim carrying the groups used for team mapping. |
| `AUTH_OIDC_USERNAME_CLAIM` | `preferred_username` | `id_token` claim used as username, `email` as fallback. |
| `AUTH_OIDC_USER_PROVISIONING` | `true` | Create accounts at first OIDC login. |
| `AUTH_OIDC_TEAM_SYNC` | `true` | Synchronize teams from the groups claim at each login. |
| `AUTH_OIDC_BUTTON_LABEL` | `Single Sign-On` | Label of the login button (64 characters max). |

When `AUTH_ANONYMOUS_PERMISSIONS` is set, its value is used as is, even when empty. When it is unset, the default is the read-only set `event:read,catalog:read,lock:read,links:read` if `DEMO_MODE=true`, otherwise every permission except `access:manage` (transitional default, with a startup warning).

See [AUTHENTICATION.md](AUTHENTICATION.md) for permissions, teams and API keys.
See [AUTHENTICATION.md](AUTHENTICATION.md) for permissions, teams and API keys, and [Single Sign-On](AUTHENTICATION.md#single-sign-on-openid-connect) for the OpenID Connect setup, redirect URI and identity provider recipes.

**Example:**
```bash
Expand All @@ -81,6 +90,15 @@ AUTH_ADMIN_PASSWORD=change-me-at-first-login
AUTH_PUBLIC_URL=https://tracker.example.com
```

**Example with OpenID Connect:**
```bash
AUTH_PUBLIC_URL=https://tracker.example.com
AUTH_OIDC_ISSUER=https://keycloak.example.com/realms/main
AUTH_OIDC_CLIENT_ID=tracker
AUTH_OIDC_CLIENT_SECRET=<from your secret store>
# Redirect URI to register: https://tracker.example.com/api/v1alpha1/auth/oidc/callback
```

### Slack Integration

| Variable | Default | Description |
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Tracker est une API de gestion d'événements, de catalogues et de verrous const
### 📚 Documentation générale
- [README](./README.md) - Vue d'ensemble et architecture
- [Spécification API](./api-specification.md) - OpenAPI et Protobuf
- [Authentication](./AUTHENTICATION.md) - Users, teams, permissions and API keys
- [Authentication](./AUTHENTICATION.md) - Users, teams, permissions, API keys and single sign-on

### 🔧 APIs par service
- [Events API](./events.md) - Gestion des événements
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module github.com/bananaops/tracker
go 1.26.1

require (
github.com/coreos/go-oidc/v3 v3.21.0
github.com/go-openapi/runtime v0.29.5
github.com/golang-jwt/jwt/v5 v5.3.0
golang.org/x/crypto v0.56.0
golang.org/x/oauth2 v0.37.0
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -15,6 +17,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-openapi/analysis v0.25.3 // indirect
github.com/go-openapi/errors v0.22.8 // indirect
github.com/go-openapi/jsonpointer v0.23.2 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down Expand Up @@ -144,6 +148,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
Expand Down
3 changes: 2 additions & 1 deletion internal/auth/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var authRequests = prometheus.NewCounterVec(
)

// AuthLogins counts login attempts. The method label names the authentication
// method (local for now, oidc once it lands) and the result label is one of
// method (local or oidc) and the result label is one of
// LoginSuccess, LoginFailure or LoginRateLimited. Malformed bodies, cross-site
// refusals and internal errors are not login attempts and are not counted.
// It is exported so the login handler, which lives in the server package, can
Expand All @@ -40,6 +40,7 @@ var AuthLogins = prometheus.NewCounterVec(
// Values of the AuthLogins labels.
const (
LoginMethodLocal = "local"
LoginMethodOIDC = "oidc"
LoginSuccess = "success"
LoginFailure = "failure"
LoginRateLimited = "rate_limited"
Expand Down
192 changes: 192 additions & 0 deletions internal/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,26 @@ package auth

import (
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net"
"net/url"
"strconv"
"strings"
"time"
"unicode/utf8"
)

const (
OIDCLoginPath = "/api/v1alpha1/auth/oidc/login"
OIDCCallbackPath = "/api/v1alpha1/auth/oidc/callback"

defaultOIDCScopes = "openid profile email"
defaultOIDCGroupsClaim = "groups"
defaultOIDCUsernameClaim = "preferred_username"
defaultOIDCButtonLabel = "Single Sign-On"
maxOIDCButtonLabelLength = 64
)

// Config is the authentication configuration read from the environment.
Expand All @@ -22,6 +39,47 @@ type Config struct {
CookieSecure bool
TrustProxy bool
DemoMode bool
// OIDC is the OpenID Connect login, disabled when Issuer is empty.
OIDC OIDCConfig
}

// OIDCConfig is the OpenID Connect login configuration read from the
// AUTH_OIDC_* environment variables.
type OIDCConfig struct {
Issuer string
ClientID string
ClientSecret string
Scopes []string
GroupsClaim string
UsernameClaim string
UserProvisioning bool
TeamSync bool
ButtonLabel string
}

// Enabled reports whether OIDC login is configured.
func (c OIDCConfig) Enabled() bool {
return c.Issuer != ""
}

// LogValue implements slog.LogValuer, omitting ClientSecret from logs.
func (c OIDCConfig) LogValue() slog.Value {
return slog.GroupValue(
slog.String("issuer", c.Issuer),
slog.String("client_id", c.ClientID),
slog.Any("scopes", c.Scopes),
slog.String("groups_claim", c.GroupsClaim),
slog.String("username_claim", c.UsernameClaim),
slog.Bool("user_provisioning", c.UserProvisioning),
slog.Bool("team_sync", c.TeamSync),
slog.String("button_label", c.ButtonLabel),
)
}

// OIDCRedirectURL is the OIDC callback URL registered with the identity
// provider.
func (c Config) OIDCRedirectURL() string {
return c.PublicURL + OIDCCallbackPath
}

// LookupEnv has the signature of os.LookupEnv.
Expand Down Expand Up @@ -96,5 +154,139 @@ func LoadConfig(lookup LookupEnv) (Config, error) {
cfg.PublicURL = strings.TrimRight(get("AUTH_PUBLIC_URL"), "/")
cfg.CookieSecure = strings.HasPrefix(cfg.PublicURL, "https://") || get("AUTH_COOKIE_SECURE") == "true"
cfg.TrustProxy = get("AUTH_TRUST_PROXY") == "true"

oidc, err := loadOIDCConfig(get, cfg.PublicURL)
if err != nil {
return Config{}, err
}
cfg.OIDC = oidc

return cfg, nil
}

// loadOIDCConfig reads the AUTH_OIDC_* variables. OIDC is off without an
// issuer. Error messages name the variable, never the client secret.
func loadOIDCConfig(get func(string) string, publicURL string) (OIDCConfig, error) {
cfg := OIDCConfig{
Issuer: get("AUTH_OIDC_ISSUER"),
ClientID: get("AUTH_OIDC_CLIENT_ID"),
ClientSecret: get("AUTH_OIDC_CLIENT_SECRET"),
}
if cfg.Issuer == "" {
if cfg.ClientID != "" || cfg.ClientSecret != "" {
return OIDCConfig{}, errors.New("AUTH_OIDC_CLIENT_ID or AUTH_OIDC_CLIENT_SECRET is set but AUTH_OIDC_ISSUER is empty")
}
return OIDCConfig{}, nil
}
if err := validateOIDCIssuer(cfg.Issuer); err != nil {
return OIDCConfig{}, err
}
if cfg.ClientID == "" {
return OIDCConfig{}, errors.New("AUTH_OIDC_CLIENT_ID is required when AUTH_OIDC_ISSUER is set")
}
if cfg.ClientSecret == "" {
return OIDCConfig{}, errors.New("AUTH_OIDC_CLIENT_SECRET is required when AUTH_OIDC_ISSUER is set")
}
if err := validateOIDCPublicURL(publicURL); err != nil {
return OIDCConfig{}, err
}
cfg.Scopes = parseOIDCScopes(getOr(get, "AUTH_OIDC_SCOPES", defaultOIDCScopes))
cfg.GroupsClaim = getOr(get, "AUTH_OIDC_GROUPS_CLAIM", defaultOIDCGroupsClaim)
cfg.UsernameClaim = getOr(get, "AUTH_OIDC_USERNAME_CLAIM", defaultOIDCUsernameClaim)
var err error
if cfg.UserProvisioning, err = boolOr(get, "AUTH_OIDC_USER_PROVISIONING", true); err != nil {
return OIDCConfig{}, err
}
if cfg.TeamSync, err = boolOr(get, "AUTH_OIDC_TEAM_SYNC", true); err != nil {
return OIDCConfig{}, err
}
cfg.ButtonLabel = getOr(get, "AUTH_OIDC_BUTTON_LABEL", defaultOIDCButtonLabel)
if utf8.RuneCountInString(cfg.ButtonLabel) > maxOIDCButtonLabelLength {
return OIDCConfig{}, fmt.Errorf("AUTH_OIDC_BUTTON_LABEL must be at most %d characters", maxOIDCButtonLabelLength)
}
return cfg, nil
}

// getOr returns def when key is unset or blank.
func getOr(get func(string) string, key, def string) string {
if v := get(key); v != "" {
return v
}
return def
}

// boolOr returns def when key is unset or blank, otherwise parses it as a
// bool. The error never repeats the value.
func boolOr(get func(string) string, key string, def bool) (bool, error) {
v := get(key)
if v == "" {
return def, nil
}
b, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("%s must be true or false", key)
}
return b, nil
}

// parseOIDCScopes splits on spaces and commas, deduplicates while keeping
// order, and puts openid first.
func parseOIDCScopes(raw string) []string {
fields := strings.FieldsFunc(raw, func(r rune) bool {
return r == ' ' || r == ','
})
seen := make(map[string]bool, len(fields)+1)
scopes := make([]string, 0, len(fields)+1)
scopes = append(scopes, "openid")
seen["openid"] = true
for _, f := range fields {
if f == "" || seen[f] {
continue
}
seen[f] = true
scopes = append(scopes, f)
}
return scopes
}

// validateOIDCIssuer requires an absolute https URL without query or
// fragment; http is accepted for loopback hosts only.
func validateOIDCIssuer(issuer string) error {
const msg = "AUTH_OIDC_ISSUER must be an absolute https URL without query or fragment (http is accepted for loopback hosts only)"
u, err := url.Parse(issuer)
if err != nil || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return errors.New(msg)
}
switch u.Scheme {
case "https":
return nil
case "http":
if isLoopbackHost(u.Hostname()) {
return nil
}
}
return errors.New(msg)
}

// isLoopbackHost reports whether h is localhost or a loopback IP.
func isLoopbackHost(h string) bool {
if h == "localhost" {
return true
}
ip := net.ParseIP(h)
return ip != nil && ip.IsLoopback()
}

// validateOIDCPublicURL requires an absolute scheme://host[:port] URL
// without a path, since it is the base of the OIDC redirect URI.
func validateOIDCPublicURL(publicURL string) error {
if publicURL == "" {
return errors.New("AUTH_PUBLIC_URL is required when AUTH_OIDC_ISSUER is set: it is the base of the OIDC redirect URI")
}
const msg = "AUTH_PUBLIC_URL must be scheme://host[:port] without path when OIDC is enabled"
u, err := url.Parse(publicURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.Path != "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return errors.New(msg)
}
return nil
}
Loading
Loading