mirror of
https://github.com/gotify/server.git
synced 2026-06-02 00:11:05 +08:00
feat: oidc
This commit is contained in:
206
api/oidc.go
Normal file
206
api/oidc.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gotify/server/v2/auth"
|
||||
"github.com/gotify/server/v2/config"
|
||||
"github.com/gotify/server/v2/database"
|
||||
"github.com/gotify/server/v2/model"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func NewOIDC(conf *config.Configuration, db *database.GormDatabase, userChangeNotifier *UserChangeNotifier) *OIDCAPI {
|
||||
scopes := conf.OIDC.Scopes
|
||||
if len(scopes) == 0 {
|
||||
scopes = []string{"openid", "profile", "email"}
|
||||
}
|
||||
|
||||
cookieKey := make([]byte, 32)
|
||||
if _, err := rand.Read(cookieKey); err != nil {
|
||||
log.Fatalf("failed to generate OIDC cookie key: %v", err)
|
||||
}
|
||||
cookieHandlerOpt := []httphelper.CookieHandlerOpt{}
|
||||
if !conf.Server.SecureCookie {
|
||||
cookieHandlerOpt = append(cookieHandlerOpt, httphelper.WithUnsecure())
|
||||
}
|
||||
cookieHandler := httphelper.NewCookieHandler(cookieKey, cookieKey, cookieHandlerOpt...)
|
||||
|
||||
opts := []rp.Option{rp.WithCookieHandler(cookieHandler), rp.WithPKCE(cookieHandler)}
|
||||
|
||||
provider, err := rp.NewRelyingPartyOIDC(
|
||||
context.Background(),
|
||||
conf.OIDC.Issuer,
|
||||
conf.OIDC.ClientID,
|
||||
conf.OIDC.ClientSecret,
|
||||
conf.OIDC.RedirectURL,
|
||||
scopes,
|
||||
opts...,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize OIDC provider: %v", err)
|
||||
}
|
||||
|
||||
return &OIDCAPI{
|
||||
DB: db,
|
||||
Provider: provider,
|
||||
UserChangeNotifier: userChangeNotifier,
|
||||
UsernameClaim: conf.OIDC.UsernameClaim,
|
||||
PasswordStrength: conf.PassStrength,
|
||||
SecureCookie: conf.Server.SecureCookie,
|
||||
AutoRegister: conf.OIDC.AutoRegister,
|
||||
}
|
||||
}
|
||||
|
||||
// OIDCAPI provides handlers for OIDC authentication.
|
||||
type OIDCAPI struct {
|
||||
DB *database.GormDatabase
|
||||
Provider rp.RelyingParty
|
||||
UserChangeNotifier *UserChangeNotifier
|
||||
UsernameClaim string
|
||||
PasswordStrength int
|
||||
SecureCookie bool
|
||||
AutoRegister bool
|
||||
}
|
||||
|
||||
// swagger:operation GET /auth/oidc/login oidc oidcLogin
|
||||
//
|
||||
// Start the OIDC login flow (browser).
|
||||
//
|
||||
// Redirects the user to the OIDC provider's authorization endpoint.
|
||||
// After authentication, the provider redirects back to the callback endpoint.
|
||||
//
|
||||
// ---
|
||||
// parameters:
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: the client name to create after login
|
||||
// required: true
|
||||
// type: string
|
||||
// responses:
|
||||
// 302:
|
||||
// description: Redirect to OIDC provider
|
||||
// default:
|
||||
// description: Error
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
func (a *OIDCAPI) LoginHandler() gin.HandlerFunc {
|
||||
return gin.WrapF(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientName := r.URL.Query().Get("name")
|
||||
if clientName == "" {
|
||||
http.Error(w, "invalid client name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
state, err := a.generateState(clientName)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to generate state: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rp.AuthURLHandler(func() string { return state }, a.Provider)(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// swagger:operation GET /auth/oidc/callback oidc oidcCallback
|
||||
//
|
||||
// Handle the OIDC provider callback (browser).
|
||||
//
|
||||
// Exchanges the authorization code for tokens, resolves the user,
|
||||
// creates a gotify client, sets a session cookie, and redirects to the UI.
|
||||
//
|
||||
// ---
|
||||
// parameters:
|
||||
// - name: code
|
||||
// in: query
|
||||
// description: the authorization code from the OIDC provider
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: state
|
||||
// in: query
|
||||
// description: the state parameter for CSRF protection
|
||||
// required: true
|
||||
// type: string
|
||||
// responses:
|
||||
// 307:
|
||||
// description: Redirect to UI
|
||||
// default:
|
||||
// description: Error
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
func (a *OIDCAPI) CallbackHandler() gin.HandlerFunc {
|
||||
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, provider rp.RelyingParty, info *oidc.UserInfo) {
|
||||
user, status, err := a.resolveUser(info)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
clientName, _, _ := strings.Cut(state, ":")
|
||||
client, err := a.createClient(clientName, user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to create client: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
auth.SetCookie(w, client.Token, auth.CookieMaxAge, a.SecureCookie)
|
||||
// A reverse proxy may have already stripped a url prefix from the URL
|
||||
// without us knowing, we have to make a relative redirect.
|
||||
// We cannot use http.Redirect as this normalizes the Path with r.URL.
|
||||
w.Header().Set("Location", "../../")
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
return gin.WrapF(rp.CodeExchangeHandler(rp.UserinfoCallback(callback), a.Provider))
|
||||
}
|
||||
|
||||
func (a *OIDCAPI) generateState(name string) (string, error) {
|
||||
nonce := make([]byte, 20)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return name + ":" + hex.EncodeToString(nonce), nil
|
||||
}
|
||||
|
||||
// resolveUser looks up or creates a user from OIDC userinfo claims.
|
||||
func (a *OIDCAPI) resolveUser(info *oidc.UserInfo) (*model.User, int, error) {
|
||||
usernameRaw, ok := info.Claims[a.UsernameClaim]
|
||||
if !ok {
|
||||
return nil, http.StatusInternalServerError, fmt.Errorf("username claim %q is missing", a.UsernameClaim)
|
||||
}
|
||||
username := fmt.Sprint(usernameRaw)
|
||||
if username == "" || usernameRaw == nil {
|
||||
return nil, http.StatusInternalServerError, fmt.Errorf("username claim was empty")
|
||||
}
|
||||
|
||||
user, err := a.DB.GetUserByName(username)
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
if !a.AutoRegister {
|
||||
return nil, http.StatusForbidden, fmt.Errorf("user does not exist and auto-registration is disabled")
|
||||
}
|
||||
user = &model.User{Name: username, Admin: false, Pass: nil}
|
||||
if err := a.DB.CreateUser(user); err != nil {
|
||||
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
if err := a.UserChangeNotifier.fireUserAdded(user.ID); err != nil {
|
||||
log.Printf("Could not notify user change: %v\n", err)
|
||||
}
|
||||
}
|
||||
return user, 0, nil
|
||||
}
|
||||
|
||||
func (a *OIDCAPI) createClient(name string, userID uint) (*model.Client, error) {
|
||||
client := &model.Client{
|
||||
Name: name,
|
||||
Token: auth.GenerateNotExistingToken(generateClientToken, func(t string) bool { c, _ := a.DB.GetClientByToken(t); return c != nil }),
|
||||
UserID: userID,
|
||||
}
|
||||
return client, a.DB.CreateClient(client)
|
||||
}
|
||||
@@ -48,6 +48,14 @@ server:
|
||||
allowedorigins: # allowed origins for websocket connections (same origin is always allowed)
|
||||
# - ".+.example.com"
|
||||
# - "otherdomain.com"
|
||||
oidc:
|
||||
enabled: false # Enable OpenID Connect login, allowing users to authenticate via an external identity provider (e.g. Keycloak, Authelia, Google).
|
||||
issuer: # The OIDC issuer URL. This is the base URL of your identity provider, used to discover endpoints. Example: "https://auth.example.com/realms/myrealm"
|
||||
clientid: # The client ID registered with your identity provider for this application.
|
||||
clientsecret: # The client secret for the registered client.
|
||||
redirecturl: http://gotify.example.org/auth/oidc/callback # The callback URL that the identity provider redirects to after authentication. Must match exactly what is configured in your identity provider.
|
||||
autoregister: true # If true, automatically create a new user on first OIDC login. If false, only existing users can log in via OIDC.
|
||||
usernameclaim: preferred_username # The OIDC claim used to determine the username. Common values: "preferred_username" or "email".
|
||||
|
||||
database: # for database see (configure database section)
|
||||
dialect: sqlite3
|
||||
|
||||
@@ -56,6 +56,16 @@ type Configuration struct {
|
||||
UploadedImagesDir string `default:"data/images"`
|
||||
PluginsDir string `default:"data/plugins"`
|
||||
Registration bool `default:"false"`
|
||||
OIDC struct {
|
||||
Enabled bool `default:"false"`
|
||||
Issuer string `default:""`
|
||||
ClientID string `default:""`
|
||||
ClientSecret string `default:""`
|
||||
UsernameClaim string `default:"preferred_username"`
|
||||
RedirectURL string `default:""`
|
||||
AutoRegister bool `default:"true"`
|
||||
Scopes []string
|
||||
}
|
||||
}
|
||||
|
||||
func configFiles() []string {
|
||||
|
||||
@@ -587,6 +587,73 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/oidc/callback": {
|
||||
"get": {
|
||||
"description": "Exchanges the authorization code for tokens, resolves the user,\ncreates a gotify client, sets a session cookie, and redirects to the UI.",
|
||||
"tags": [
|
||||
"oidc"
|
||||
],
|
||||
"summary": "Handle the OIDC provider callback (browser).",
|
||||
"operationId": "oidcCallback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the authorization code from the OIDC provider",
|
||||
"name": "code",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the state parameter for CSRF protection",
|
||||
"name": "state",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"307": {
|
||||
"description": "Redirect to UI"
|
||||
},
|
||||
"default": {
|
||||
"description": "Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/oidc/login": {
|
||||
"get": {
|
||||
"description": "Redirects the user to the OIDC provider's authorization endpoint.\nAfter authentication, the provider redirects back to the callback endpoint.",
|
||||
"tags": [
|
||||
"oidc"
|
||||
],
|
||||
"summary": "Start the OIDC login flow (browser).",
|
||||
"operationId": "oidcLogin",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the client name to create after login",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Redirect to OIDC provider"
|
||||
},
|
||||
"default": {
|
||||
"description": "Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/client": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
||||
16
go.mod
16
go.mod
@@ -14,6 +14,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/zitadel/oidc/v3 v3.45.5
|
||||
golang.org/x/crypto v0.48.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
@@ -28,15 +29,21 @@ require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
@@ -49,15 +56,24 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muhlemmer/gu v0.3.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/zitadel/logging v0.7.0 // indirect
|
||||
github.com/zitadel/schema v1.3.2 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
|
||||
51
go.sum
51
go.sum
@@ -3,12 +3,16 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -28,6 +32,15 @@ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fq
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -46,6 +59,12 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/location v0.0.0-20170722210143-03bc4ad20437 h1:4qMhogAexRcnvdoY9O1RoCuuuNEhDF25jtbGIWPtcms=
|
||||
@@ -62,6 +81,8 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
|
||||
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||
github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA=
|
||||
github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -90,6 +111,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
|
||||
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
|
||||
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
|
||||
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -100,8 +125,12 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -119,8 +148,22 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
|
||||
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ=
|
||||
github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ=
|
||||
github.com/zitadel/oidc/v3 v3.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrDQ=
|
||||
github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE=
|
||||
github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI=
|
||||
github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
@@ -130,6 +173,8 @@ golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVo
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
@@ -147,6 +192,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -104,7 +104,14 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||
userChangeNotifier.OnUserDeleted(pluginManager.RemoveUser)
|
||||
userChangeNotifier.OnUserAdded(pluginManager.InitializeForUserID)
|
||||
|
||||
ui.Register(g, *vInfo, conf.Registration)
|
||||
ui.Register(g, *vInfo, conf.Registration, conf.OIDC.Enabled)
|
||||
|
||||
if conf.OIDC.Enabled {
|
||||
oidcHandler := api.NewOIDC(conf, db, userChangeNotifier)
|
||||
oidcGroup := g.Group("/auth/oidc")
|
||||
oidcGroup.GET("/login", oidcHandler.LoginHandler())
|
||||
oidcGroup.GET("/callback", oidcHandler.CallbackHandler())
|
||||
}
|
||||
|
||||
g.Match([]string{"GET", "HEAD"}, "/health", healthHandler.Health)
|
||||
g.GET("/swagger", docs.Serve)
|
||||
|
||||
@@ -18,11 +18,12 @@ var box embed.FS
|
||||
type uiConfig struct {
|
||||
Register bool `json:"register"`
|
||||
Version model.VersionInfo `json:"version"`
|
||||
OIDC bool `json:"oidc"`
|
||||
}
|
||||
|
||||
// Register registers the ui on the root path.
|
||||
func Register(r *gin.Engine, version model.VersionInfo, register bool) {
|
||||
uiConfigBytes, err := json.Marshal(uiConfig{Version: version, Register: register})
|
||||
func Register(r *gin.Engine, version model.VersionInfo, register, oidcEnabled bool) {
|
||||
uiConfigBytes, err := json.Marshal(uiConfig{Version: version, Register: register, OIDC: oidcEnabled})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -38,13 +38,17 @@ export class CurrentUser {
|
||||
return false;
|
||||
});
|
||||
|
||||
public createClientName = (): string => {
|
||||
const browser = detect();
|
||||
return (browser && browser.name + ' ' + browser.version) || 'unknown browser';
|
||||
};
|
||||
|
||||
public login = async (username: string, password: string) => {
|
||||
runInAction(() => {
|
||||
this.loggedIn = false;
|
||||
this.authenticating = true;
|
||||
});
|
||||
const browser = detect();
|
||||
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
|
||||
const name = this.createClientName();
|
||||
axios
|
||||
.create()
|
||||
.request({
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface IConfig {
|
||||
url: string;
|
||||
register: boolean;
|
||||
version: IVersion;
|
||||
oidc: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -16,6 +17,7 @@ const config: IConfig = {
|
||||
url: 'unset',
|
||||
register: false,
|
||||
version: {commit: 'unknown', buildDate: 'unknown', version: 'unknown'},
|
||||
oidc: false,
|
||||
...window.config,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import React from 'react';
|
||||
@@ -80,6 +81,24 @@ const Login = observer(() => {
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
{config.get('oidc') && (
|
||||
<>
|
||||
<Divider style={{marginTop: 15, marginBottom: 15}}>or</Divider>
|
||||
<Button
|
||||
component="a"
|
||||
href={
|
||||
config.get('url') +
|
||||
'auth/oidc/login?name=' +
|
||||
encodeURIComponent(currentUser.createClientName())
|
||||
}
|
||||
variant="outlined"
|
||||
size="large"
|
||||
color="primary"
|
||||
style={{marginBottom: 5}}>
|
||||
Login with OIDC
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</Grid>
|
||||
{registerDialog && (
|
||||
|
||||
Reference in New Issue
Block a user