Skip to content

Commit f2b947a

Browse files
committed
Add embedded oidc provider for quick start
Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt> On-behalf-of: @SAP mangirdas.judeikis@sap.com
1 parent f1cbbdb commit f2b947a

17 files changed

Lines changed: 678 additions & 63 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ apiserviceexport.yaml
2020
# Frontend dependencies and build
2121
web/node_modules/
2222
web/.vite/
23-
web/*.tsbuildinfo
23+
web/*.tsbuildinfo
24+
go.work
25+
go.work.sum

backend/auth/handler.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package auth
1818

1919
import (
20+
"context"
2021
"encoding/base64"
2122
"encoding/json"
2223
"errors"
@@ -34,14 +35,23 @@ import (
3435
"github.com/kube-bind/kube-bind/backend/session"
3536
)
3637

38+
type OIDCProvider interface {
39+
GetOIDCProvider(ctx context.Context) (*OIDCServiceProvider, error)
40+
}
41+
42+
type AuthHandlerInterface interface {
43+
HandleAuthorize(w http.ResponseWriter, r *http.Request)
44+
HandleCallback(w http.ResponseWriter, r *http.Request)
45+
}
46+
3747
type AuthHandler struct {
38-
oidc *OIDCServiceProvider
48+
oidc OIDCProvider
3949
jwtService *JWTService
4050
cookieSigningKey []byte
4151
cookieEncryptionKey []byte
4252
}
4353

44-
func NewAuthHandler(oidc *OIDCServiceProvider, jwtService *JWTService, cookieSigningKey, cookieEncryptionKey []byte) *AuthHandler {
54+
func NewAuthHandler(oidc OIDCProvider, jwtService *JWTService, cookieSigningKey, cookieEncryptionKey []byte) *AuthHandler {
4555
return &AuthHandler{
4656
oidc: oidc,
4757
jwtService: jwtService,
@@ -85,8 +95,15 @@ func (ah *AuthHandler) HandleAuthorize(w http.ResponseWriter, r *http.Request) {
8595
return
8696
}
8797

98+
provider, err := ah.oidc.GetOIDCProvider(r.Context())
99+
if err != nil {
100+
logger.Info("failed to get OIDC provider", "error", err)
101+
ah.respondWithError(w, authReq.ClientType, err.Error(), http.StatusInternalServerError)
102+
return
103+
}
104+
88105
encoded := base64.URLEncoding.EncodeToString(dataCode)
89-
authURL := ah.oidc.OIDCProviderConfig(scopes).AuthCodeURL(encoded)
106+
authURL := provider.OIDCProviderConfig(scopes).AuthCodeURL(encoded)
90107

91108
http.Redirect(w, r, authURL, http.StatusFound)
92109
}
@@ -133,7 +150,25 @@ func (ah *AuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
133150
return
134151
}
135152

136-
token, err := ah.oidc.OIDCProviderConfig(nil).Exchange(r.Context(), code)
153+
provider, err := ah.oidc.GetOIDCProvider(r.Context())
154+
if err != nil {
155+
logger.Info("failed to get OIDC provider", "error", err)
156+
ah.respondWithError(w, authCode.ClientType, err.Error(), http.StatusInternalServerError)
157+
return
158+
}
159+
160+
// Create context with custom HTTP client if TLS config is available
161+
ctx := r.Context()
162+
if tlsConfig := provider.GetTLSConfig(); tlsConfig != nil {
163+
client := &http.Client{
164+
Transport: &http.Transport{
165+
TLSClientConfig: tlsConfig,
166+
},
167+
}
168+
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
169+
}
170+
171+
token, err := provider.OIDCProviderConfig(nil).Exchange(ctx, code)
137172
if err != nil {
138173
logger.Error(err, "failed to exchange token")
139174
http.Error(w, "internal error", http.StatusInternalServerError)

backend/auth/types.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package auth
1818

1919
import (
2020
"context"
21+
"crypto/tls"
22+
"net/http"
2123
"time"
2224

2325
oidc "github.com/coreos/go-oidc/v3/oidc"
@@ -67,8 +69,9 @@ type OIDCServiceProvider struct {
6769
redirectURI string
6870
issuerURL string
6971

70-
verifier *oidc.IDTokenVerifier
71-
provider *oidc.Provider
72+
verifier *oidc.IDTokenVerifier
73+
provider *oidc.Provider
74+
tlsConfig *tls.Config
7275
}
7376

7477
func NewOIDCServiceProvider(ctx context.Context, clientID, clientSecret, redirectURI, issuerURL string) (*OIDCServiceProvider, error) {
@@ -84,15 +87,53 @@ func NewOIDCServiceProvider(ctx context.Context, clientID, clientSecret, redirec
8487
issuerURL: issuerURL,
8588
provider: provider,
8689
verifier: provider.Verifier(&oidc.Config{ClientID: clientID}),
90+
tlsConfig: nil,
91+
}, nil
92+
}
93+
94+
func NewOIDCServiceProviderWithTLS(
95+
ctx context.Context,
96+
clientID, clientSecret, redirectURI, issuerURL string,
97+
tlsConfig *tls.Config,
98+
) (*OIDCServiceProvider, error) {
99+
// Create a custom HTTP client that trusts the TLS config
100+
client := &http.Client{
101+
Transport: &http.Transport{
102+
TLSClientConfig: tlsConfig,
103+
},
104+
}
105+
106+
// Create context with the custom client
107+
ctxWithClient := oidc.ClientContext(ctx, client)
108+
109+
provider, err := oidc.NewProvider(ctxWithClient, issuerURL)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
return &OIDCServiceProvider{
115+
clientID: clientID,
116+
clientSecret: clientSecret,
117+
redirectURI: redirectURI,
118+
issuerURL: issuerURL,
119+
provider: provider,
120+
verifier: provider.Verifier(&oidc.Config{ClientID: clientID}),
121+
tlsConfig: tlsConfig,
87122
}, nil
88123
}
89124

90125
func (o *OIDCServiceProvider) OIDCProviderConfig(scopes []string) *oauth2.Config {
91-
return &oauth2.Config{
126+
config := &oauth2.Config{
92127
ClientID: o.clientID,
93128
ClientSecret: o.clientSecret,
94129
Endpoint: o.provider.Endpoint(),
95130
RedirectURL: o.redirectURI,
96131
Scopes: scopes,
97132
}
133+
134+
return config
135+
}
136+
137+
func (o *OIDCServiceProvider) GetTLSConfig() *tls.Config {
138+
return o.tlsConfig
98139
}

backend/http/handler.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/kube-bind/kube-bind/backend/auth"
3535
"github.com/kube-bind/kube-bind/backend/client"
3636
"github.com/kube-bind/kube-bind/backend/kubernetes"
37+
"github.com/kube-bind/kube-bind/backend/oidc"
3738
"github.com/kube-bind/kube-bind/backend/spaserver"
3839
bindversion "github.com/kube-bind/kube-bind/pkg/version"
3940
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
@@ -48,8 +49,8 @@ var noCacheHeaders = map[string]string{
4849
}
4950

5051
type handler struct {
51-
oidc *auth.OIDCServiceProvider
52-
authHandler *auth.AuthHandler
52+
oidcProvider auth.OIDCProvider
53+
authHandler auth.AuthHandlerInterface
5354
authMiddleware *auth.AuthMiddleware
5455

5556
scope kubebindv1alpha2.InformerScope
@@ -64,12 +65,14 @@ type handler struct {
6465

6566
client *http.Client
6667
kubeManager *kubernetes.Manager
68+
oidcServer *oidc.Server
6769

6870
frontend string
6971
}
7072

7173
func NewHandler(
72-
provider *auth.OIDCServiceProvider,
74+
oidcProvider auth.OIDCProvider,
75+
oidcServer *oidc.Server,
7376
oidcAuthorizeURL, backendCallbackURL, providerPrettyName, testingAutoSelect string,
7477
cookieSigningKey, cookieEncryptionKey []byte,
7578
schemaSource string,
@@ -83,14 +86,14 @@ func NewHandler(
8386
return nil, fmt.Errorf("failed to create JWT service: %w", err)
8487
}
8588

86-
// Create auth handler for generic authentication flows
87-
authHandler := auth.NewAuthHandler(provider, jwtService, cookieSigningKey, cookieEncryptionKey)
89+
// Create auth handler with OIDC provider
90+
authHandler := auth.NewAuthHandler(oidcProvider, jwtService, cookieSigningKey, cookieEncryptionKey)
8891

8992
// Create auth middleware for request authentication
9093
authMiddleware := auth.NewAuthMiddleware(jwtService, cookieSigningKey, cookieEncryptionKey)
9194

9295
return &handler{
93-
oidc: provider,
96+
oidcProvider: oidcProvider,
9497
authHandler: authHandler,
9598
authMiddleware: authMiddleware,
9699
oidcAuthorizeURL: oidcAuthorizeURL,
@@ -104,6 +107,7 @@ func NewHandler(
104107
kubeManager: mgr,
105108
cookieSigningKey: cookieSigningKey,
106109
cookieEncryptionKey: cookieEncryptionKey,
110+
oidcServer: oidcServer,
107111
}, nil
108112
}
109113

@@ -129,6 +133,10 @@ func (h *handler) AddRoutes(mux *mux.Router) error {
129133
apiRouter.Handle("/bind", auth.RequireAuth(http.HandlerFunc(h.handleBind))).Methods(http.MethodPost)
130134
apiRouter.Handle("/ping", auth.RequireAuth(http.HandlerFunc(h.handlePing))).Methods(http.MethodGet)
131135

136+
if h.oidcServer != nil {
137+
h.oidcServer.AddRoutes(mux)
138+
}
139+
132140
switch {
133141
// Development mode: proxy to frontend dev server
134142
case strings.HasPrefix(h.frontend, "http://"):

backend/http/server.go

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"log"
2222
"net"
2323
"net/http"
24-
"strconv"
2524
"time"
2625

2726
"github.com/gorilla/mux"
@@ -37,22 +36,9 @@ type Server struct {
3736

3837
func NewServer(options *options.Serve) (*Server, error) {
3938
server := &Server{
40-
options: options,
41-
Router: mux.NewRouter(),
42-
}
43-
44-
if options.Listener == nil {
45-
var err error
46-
addr := options.ListenAddress
47-
if options.ListenIP != "" {
48-
addr = net.JoinHostPort(options.ListenIP, strconv.Itoa(options.ListenPort))
49-
}
50-
server.listener, err = net.Listen("tcp", addr)
51-
if err != nil {
52-
return nil, err
53-
}
54-
} else {
55-
server.listener = options.Listener
39+
options: options,
40+
Router: mux.NewRouter(),
41+
listener: options.Listener,
5642
}
5743

5844
return server, nil

backend/oidc/oidc.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
Copyright 2025 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package oidc
18+
19+
import (
20+
"crypto/tls"
21+
"crypto/x509"
22+
"fmt"
23+
"net"
24+
"os"
25+
"path"
26+
"time"
27+
28+
"github.com/gorilla/mux"
29+
"github.com/xrstf/mockoidc"
30+
)
31+
32+
type Server struct {
33+
server *mockoidc.MockOIDC
34+
tlsConfig *tls.Config
35+
}
36+
37+
func New(caBundleFile string, listener net.Listener) (*Server, error) {
38+
// Add offline_access to supported scopes for refresh token support
39+
ensureOfflineAccessScope()
40+
var tlsConfig *tls.Config
41+
s := &Server{}
42+
if caBundleFile != "" {
43+
var err error
44+
tlsConfig, err = LoadTLSConfig(caBundleFile)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to load CA bundle file: %w", err)
47+
}
48+
s.tlsConfig = tlsConfig
49+
}
50+
51+
server, err := mockoidc.NewServer(&mockoidc.ServerConfig{
52+
TLSConfig: tlsConfig,
53+
Listener: listener,
54+
})
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to create mock OIDC server: %w", err)
57+
}
58+
s.server = server
59+
return s, nil
60+
}
61+
62+
// Config gives the configuration for clients to connect to the embedded OIDC server.
63+
type Config struct {
64+
ClientID string
65+
ClientSecret string
66+
Issuer string
67+
68+
AccessTTL time.Duration
69+
RefreshTTL time.Duration
70+
71+
CodeChallengeMethodsSupported []string
72+
73+
// CallbackURL is kube-bind specific and must match API server endpoints.
74+
CallbackURL string
75+
}
76+
77+
var ErrServerNotRunning = fmt.Errorf("embedded OIDC server is not running")
78+
79+
func (s *Server) TLSConfig() *tls.Config {
80+
return s.tlsConfig
81+
}
82+
83+
func (s *Server) AddRoutes(mux *mux.Router) {
84+
s.server.AddRoutes(mux)
85+
}
86+
87+
// URL returns the base URL of the embedded OIDC server.
88+
func (s *Server) Config() (*Config, error) {
89+
return &Config{
90+
ClientID: s.server.Config().ClientID,
91+
ClientSecret: s.server.Config().ClientSecret,
92+
Issuer: s.server.Config().Issuer,
93+
94+
AccessTTL: s.server.Config().AccessTTL,
95+
RefreshTTL: s.server.Config().RefreshTTL,
96+
97+
CodeChallengeMethodsSupported: s.server.Config().CodeChallengeMethodsSupported,
98+
CallbackURL: path.Join(s.server.Addr(), "api/callback"),
99+
}, nil
100+
}
101+
102+
func LoadTLSConfig(caFile string) (*tls.Config, error) {
103+
caCertPool := x509.NewCertPool()
104+
caCert, err := os.ReadFile(caFile)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to read CA file: %w", err)
107+
}
108+
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
109+
return nil, fmt.Errorf("failed to append CA certs from PEM")
110+
}
111+
return &tls.Config{
112+
RootCAs: caCertPool,
113+
}, nil
114+
}
115+
116+
// ensureOfflineAccessScope adds "offline_access" to mockoidc's supported scopes if not already present
117+
func ensureOfflineAccessScope() {
118+
offlineAccess := "offline_access"
119+
120+
// Check if offline_access is already in the supported scopes
121+
for _, scope := range mockoidc.ScopesSupported {
122+
if scope == offlineAccess {
123+
return // Already present
124+
}
125+
}
126+
127+
// Add offline_access to the supported scopes
128+
mockoidc.ScopesSupported = append(mockoidc.ScopesSupported, offlineAccess)
129+
}

0 commit comments

Comments
 (0)