mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-11 16:17:39 +02:00
controlplane: move jwks.json endpoint to control plane (#3691)
This commit is contained in:
parent
63b210e51d
commit
b68dc1ff4f
6 changed files with 99 additions and 70 deletions
|
@ -74,7 +74,6 @@ func (a *Authenticate) Mount(r *mux.Router) {
|
||||||
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet)
|
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet)
|
||||||
|
|
||||||
a.mountDashboard(r)
|
a.mountDashboard(r)
|
||||||
a.mountWellKnown(r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticate) mountDashboard(r *mux.Router) {
|
func (a *Authenticate) mountDashboard(r *mux.Router) {
|
||||||
|
@ -112,19 +111,6 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
|
||||||
cr.Path("/").Handler(a.requireValidSignature(a.Callback)).Methods(http.MethodGet)
|
cr.Path("/").Handler(a.requireValidSignature(a.Callback)).Methods(http.MethodGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticate) mountWellKnown(r *mux.Router) {
|
|
||||||
r.Path("/.well-known/pomerium/jwks.json").Handler(cors.AllowAll().Handler(httputil.HandlerFunc(a.jwks))).Methods(http.MethodGet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwks returns the signing key(s) the client can use to validate signatures
|
|
||||||
// from the authorization server.
|
|
||||||
//
|
|
||||||
// https://tools.ietf.org/html/rfc8414
|
|
||||||
func (a *Authenticate) jwks(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
httputil.RenderJSON(w, http.StatusOK, a.state.Load().jwk)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetrieveSession is the middleware used retrieve session by the sessionLoaders
|
// RetrieveSession is the middleware used retrieve session by the sessionLoaders
|
||||||
func (a *Authenticate) RetrieveSession(next http.Handler) http.Handler {
|
func (a *Authenticate) RetrieveSession(next http.Handler) http.Handler {
|
||||||
return sessions.RetrieveSession(a.state.Load().sessionLoaders...)(next)
|
return sessions.RetrieveSession(a.state.Load().sessionLoaders...)(next)
|
||||||
|
|
|
@ -595,27 +595,6 @@ func TestAuthenticate_SessionValidatorMiddleware(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJwksEndpoint(t *testing.T) {
|
|
||||||
o := newTestOptions(t)
|
|
||||||
o.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpCMFZkbko1VjEvbVlpYUlIWHhnd2Q0Yzd5YWRTeXMxb3Y0bzA1b0F3ekdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUc1eENQMEpUVDFINklvbDhqS3VUSVBWTE0wNENnVzlQbEV5cE5SbVdsb29LRVhSOUhUMwpPYnp6aktZaWN6YjArMUt3VjJmTVRFMTh1dy82MXJVQ0JBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
|
||||||
auth, err := New(&config.Config{Options: o})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h := auth.Handler()
|
|
||||||
if h == nil {
|
|
||||||
t.Error("handler cannot be nil")
|
|
||||||
}
|
|
||||||
req := httptest.NewRequest("GET", "/.well-known/pomerium/jwks.json", nil)
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
body := rr.Body.String()
|
|
||||||
expected := "{\"keys\":[{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"5b419ade1895fec2d2def6cd33b1b9a018df60db231dc5ecb85cbed6d942813c\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"UG5xCP0JTT1H6Iol8jKuTIPVLM04CgW9PlEypNRmWlo\",\"y\":\"KChF0fR09zm884ymInM29PtSsFdnzExNfLsP-ta1AgQ\"}]}\n"
|
|
||||||
assert.Equal(t, expected, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthenticate_userInfo(t *testing.T) {
|
func TestAuthenticate_userInfo(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,14 @@
|
||||||
package controlplane
|
package controlplane
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/CAFxX/httpcompression"
|
"github.com/CAFxX/httpcompression"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/pomerium/csrf"
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
@ -47,32 +46,23 @@ func (srv *Server) addHTTPMiddleware(root *mux.Router, cfg *config.Config) {
|
||||||
root.Use(telemetry.HTTPStatsHandler(func() string {
|
root.Use(telemetry.HTTPStatsHandler(func() string {
|
||||||
return srv.currentConfig.Load().Options.InstallationID
|
return srv.currentConfig.Load().Options.InstallationID
|
||||||
}, srv.name))
|
}, srv.name))
|
||||||
root.HandleFunc("/healthz", httputil.HealthCheck)
|
|
||||||
root.HandleFunc("/ping", httputil.HealthCheck)
|
|
||||||
root.Handle("/.well-known/pomerium", httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return wellKnownPomerium(w, r, cfg)
|
|
||||||
}))
|
|
||||||
root.Handle("/.well-known/pomerium/", httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return wellKnownPomerium(w, r, cfg)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func wellKnownPomerium(w http.ResponseWriter, r *http.Request, cfg *config.Config) error {
|
func (srv *Server) mountCommonEndpoints(root *mux.Router, cfg *config.Config) error {
|
||||||
authenticateURL, err := cfg.Options.GetAuthenticateURL()
|
authenticateURL, err := cfg.Options.GetAuthenticateURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("invalid authenticate URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wellKnownURLs := struct {
|
rawSigningKey, err := cfg.Options.GetSigningKey()
|
||||||
OAuth2Callback string `json:"authentication_callback_endpoint"` // RFC6749
|
if err != nil {
|
||||||
JSONWebKeySetURL string `json:"jwks_uri"` // RFC7517
|
return fmt.Errorf("invalid signing key: %w", err)
|
||||||
FrontchannelLogoutURI string `json:"frontchannel_logout_uri"` // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
|
||||||
}{
|
|
||||||
authenticateURL.ResolveReference(&url.URL{Path: "/oauth2/callback"}).String(),
|
|
||||||
authenticateURL.ResolveReference(&url.URL{Path: "/.well-known/pomerium/jwks.json"}).String(),
|
|
||||||
authenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_out"}).String(),
|
|
||||||
}
|
}
|
||||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
|
||||||
httputil.RenderJSON(w, http.StatusOK, wellKnownURLs)
|
root.HandleFunc("/healthz", httputil.HealthCheck)
|
||||||
|
root.HandleFunc("/ping", httputil.HealthCheck)
|
||||||
|
root.Handle("/.well-known/pomerium", httputil.WellKnownPomeriumHandler(authenticateURL))
|
||||||
|
root.Handle("/.well-known/pomerium/", httputil.WellKnownPomeriumHandler(authenticateURL))
|
||||||
|
root.Path("/.well-known/pomerium/jwks.json").Methods(http.MethodGet).Handler(httputil.JWKSHandler(rawSigningKey))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,6 +285,9 @@ func (srv *Server) EnableProxy(svc Service) error {
|
||||||
func (srv *Server) updateRouter(cfg *config.Config) error {
|
func (srv *Server) updateRouter(cfg *config.Config) error {
|
||||||
httpRouter := mux.NewRouter()
|
httpRouter := mux.NewRouter()
|
||||||
srv.addHTTPMiddleware(httpRouter, cfg)
|
srv.addHTTPMiddleware(httpRouter, cfg)
|
||||||
|
if err := srv.mountCommonEndpoints(httpRouter, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if srv.authenticateSvc != nil {
|
if srv.authenticateSvc != nil {
|
||||||
authenticateURL, err := cfg.Options.GetInternalAuthenticateURL()
|
authenticateURL, err := cfg.Options.GetInternalAuthenticateURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/pkg/netutil"
|
"github.com/pomerium/pomerium/pkg/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerWellKnown(t *testing.T) {
|
func TestServerHTTP(t *testing.T) {
|
||||||
ports, err := netutil.AllocatePorts(5)
|
ports, err := netutil.AllocatePorts(5)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -33,11 +33,14 @@ func TestServerWellKnown(t *testing.T) {
|
||||||
Options: config.NewDefaultOptions(),
|
Options: config.NewDefaultOptions(),
|
||||||
}
|
}
|
||||||
cfg.Options.AuthenticateURLString = "https://authenticate.localhost.pomerium.io"
|
cfg.Options.AuthenticateURLString = "https://authenticate.localhost.pomerium.io"
|
||||||
|
cfg.Options.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpCMFZkbko1VjEvbVlpYUlIWHhnd2Q0Yzd5YWRTeXMxb3Y0bzA1b0F3ekdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUc1eENQMEpUVDFINklvbDhqS3VUSVBWTE0wNENnVzlQbEV5cE5SbVdsb29LRVhSOUhUMwpPYnp6aktZaWN6YjArMUt3VjJmTVRFMTh1dy82MXJVQ0JBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
||||||
|
|
||||||
src := config.NewStaticSource(cfg)
|
src := config.NewStaticSource(cfg)
|
||||||
srv, err := NewServer(cfg, config.NewMetricsManager(ctx, src), events.New())
|
srv, err := NewServer(cfg, config.NewMetricsManager(ctx, src), events.New())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
go srv.Run(ctx)
|
go srv.Run(ctx)
|
||||||
|
|
||||||
|
t.Run("well-known", func(t *testing.T) {
|
||||||
res, err := http.Get(fmt.Sprintf("http://localhost:%s/.well-known/pomerium", src.GetConfig().HTTPPort))
|
res, err := http.Get(fmt.Sprintf("http://localhost:%s/.well-known/pomerium", src.GetConfig().HTTPPort))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
@ -52,4 +55,27 @@ func TestServerWellKnown(t *testing.T) {
|
||||||
"jwks_uri": "https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json",
|
"jwks_uri": "https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json",
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, actual)
|
assert.Equal(t, expect, actual)
|
||||||
|
})
|
||||||
|
t.Run("jwks", func(t *testing.T) {
|
||||||
|
res, err := http.Get(fmt.Sprintf("http://localhost:%s/.well-known/pomerium/jwks.json", src.GetConfig().HTTPPort))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
var actual map[string]any
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&actual)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expect := map[string]any{
|
||||||
|
"keys": []any{map[string]any{
|
||||||
|
"alg": "ES256",
|
||||||
|
"crv": "P-256",
|
||||||
|
"kid": "5b419ade1895fec2d2def6cd33b1b9a018df60db231dc5ecb85cbed6d942813c",
|
||||||
|
"kty": "EC",
|
||||||
|
"use": "sig",
|
||||||
|
"x": "UG5xCP0JTT1H6Iol8jKuTIPVLM04CgW9PlEypNRmWlo",
|
||||||
|
"y": "KChF0fR09zm884ymInM29PtSsFdnzExNfLsP-ta1AgQ",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expect, actual)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,17 @@ package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v3"
|
||||||
|
|
||||||
|
"github.com/pomerium/csrf"
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthCheck is a simple healthcheck handler that responds to GET and HEAD
|
// HealthCheck is a simple healthcheck handler that responds to GET and HEAD
|
||||||
|
@ -64,3 +71,41 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
e.ErrorResponse(r.Context(), w, r)
|
e.ErrorResponse(r.Context(), w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JWKSHandler returns the /.well-known/pomerium/jwks.json handler.
|
||||||
|
func JWKSHandler(rawSigningKey string) http.Handler {
|
||||||
|
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
var jwks jose.JSONWebKeySet
|
||||||
|
if rawSigningKey != "" {
|
||||||
|
decodedCert, err := base64.StdEncoding.DecodeString(rawSigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(http.StatusInternalServerError, errors.New("bad signing key"))
|
||||||
|
}
|
||||||
|
jwk, err := cryptutil.PublicJWKFromBytes(decodedCert)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(http.StatusInternalServerError, errors.New("bad signing key"))
|
||||||
|
}
|
||||||
|
jwks.Keys = append(jwks.Keys, *jwk)
|
||||||
|
}
|
||||||
|
RenderJSON(w, http.StatusOK, jwks)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownPomeriumHandler returns the /.well-known/pomerium handler.
|
||||||
|
func WellKnownPomeriumHandler(authenticateURL *url.URL) http.Handler {
|
||||||
|
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
wellKnownURLs := struct {
|
||||||
|
OAuth2Callback string `json:"authentication_callback_endpoint"` // RFC6749
|
||||||
|
JSONWebKeySetURL string `json:"jwks_uri"` // RFC7517
|
||||||
|
FrontchannelLogoutURI string `json:"frontchannel_logout_uri"` // https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
||||||
|
}{
|
||||||
|
authenticateURL.ResolveReference(&url.URL{Path: "/oauth2/callback"}).String(),
|
||||||
|
authenticateURL.ResolveReference(&url.URL{Path: "/.well-known/pomerium/jwks.json"}).String(),
|
||||||
|
authenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_out"}).String(),
|
||||||
|
}
|
||||||
|
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||||
|
RenderJSON(w, http.StatusOK, wellKnownURLs)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue