controlplane: move jwks.json endpoint to control plane (#3691)

This commit is contained in:
Caleb Doxsey 2022-10-25 08:01:33 -06:00 committed by GitHub
parent 63b210e51d
commit b68dc1ff4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 99 additions and 70 deletions

View file

@ -74,7 +74,6 @@ func (a *Authenticate) Mount(r *mux.Router) {
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet)
a.mountDashboard(r)
a.mountWellKnown(r)
}
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)
}
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
func (a *Authenticate) RetrieveSession(next http.Handler) http.Handler {
return sessions.RetrieveSession(a.state.Load().sessionLoaders...)(next)

View file

@ -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) {
t.Parallel()

View file

@ -2,15 +2,14 @@
package controlplane
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/CAFxX/httpcompression"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pomerium/csrf"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/httputil"
"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 {
return srv.currentConfig.Load().Options.InstallationID
}, 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()
if err != nil {
return err
return fmt.Errorf("invalid authenticate URL: %w", err)
}
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(),
rawSigningKey, err := cfg.Options.GetSigningKey()
if err != nil {
return fmt.Errorf("invalid signing key: %w", err)
}
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
}

View file

@ -285,6 +285,9 @@ func (srv *Server) EnableProxy(svc Service) error {
func (srv *Server) updateRouter(cfg *config.Config) error {
httpRouter := mux.NewRouter()
srv.addHTTPMiddleware(httpRouter, cfg)
if err := srv.mountCommonEndpoints(httpRouter, cfg); err != nil {
return err
}
if srv.authenticateSvc != nil {
authenticateURL, err := cfg.Options.GetInternalAuthenticateURL()
if err != nil {

View file

@ -15,7 +15,7 @@ import (
"github.com/pomerium/pomerium/pkg/netutil"
)
func TestServerWellKnown(t *testing.T) {
func TestServerHTTP(t *testing.T) {
ports, err := netutil.AllocatePorts(5)
require.NoError(t, err)
@ -33,23 +33,49 @@ func TestServerWellKnown(t *testing.T) {
Options: config.NewDefaultOptions(),
}
cfg.Options.AuthenticateURLString = "https://authenticate.localhost.pomerium.io"
cfg.Options.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpCMFZkbko1VjEvbVlpYUlIWHhnd2Q0Yzd5YWRTeXMxb3Y0bzA1b0F3ekdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUc1eENQMEpUVDFINklvbDhqS3VUSVBWTE0wNENnVzlQbEV5cE5SbVdsb29LRVhSOUhUMwpPYnp6aktZaWN6YjArMUt3VjJmTVRFMTh1dy82MXJVQ0JBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
src := config.NewStaticSource(cfg)
srv, err := NewServer(cfg, config.NewMetricsManager(ctx, src), events.New())
require.NoError(t, err)
go srv.Run(ctx)
res, err := http.Get(fmt.Sprintf("http://localhost:%s/.well-known/pomerium", src.GetConfig().HTTPPort))
require.NoError(t, err)
defer res.Body.Close()
t.Run("well-known", func(t *testing.T) {
res, err := http.Get(fmt.Sprintf("http://localhost:%s/.well-known/pomerium", 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)
var actual map[string]any
err = json.NewDecoder(res.Body).Decode(&actual)
require.NoError(t, err)
expect := map[string]any{
"authentication_callback_endpoint": "https://authenticate.localhost.pomerium.io/oauth2/callback",
"frontchannel_logout_uri": "https://authenticate.localhost.pomerium.io/.pomerium/sign_out",
"jwks_uri": "https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json",
}
assert.Equal(t, expect, actual)
expect := map[string]any{
"authentication_callback_endpoint": "https://authenticate.localhost.pomerium.io/oauth2/callback",
"frontchannel_logout_uri": "https://authenticate.localhost.pomerium.io/.pomerium/sign_out",
"jwks_uri": "https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json",
}
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)
})
}

View file

@ -2,10 +2,17 @@ package httputil
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"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
@ -64,3 +71,41 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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
})
}