Skip to content

Commit

Permalink
authenticate: add jwks and .well-known endpoint (#745)
Browse files Browse the repository at this point in the history
Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
  • Loading branch information
desimone committed May 21, 2020
1 parent 9b82954 commit 3f1faf2
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 78 deletions.
24 changes: 21 additions & 3 deletions authenticate/authenticate.go
Expand Up @@ -10,6 +10,8 @@ import (
"html/template"
"net/url"

"gopkg.in/square/go-jose.v2"

"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/encoding"
Expand Down Expand Up @@ -95,6 +97,8 @@ type Authenticate struct {
// cacheClient is the interface for setting and getting sessions from a cache
cacheClient cache.Cacher

jwk *jose.JSONWebKeySet

templates *template.Template
}

Expand Down Expand Up @@ -166,7 +170,7 @@ func New(opts config.Options) (*Authenticate, error) {
return nil, err
}

return &Authenticate{
a := &Authenticate{
RedirectURL: redirectURL,
// shared state
sharedKey: opts.SharedKey,
Expand All @@ -183,7 +187,21 @@ func New(opts config.Options) (*Authenticate, error) {
provider: provider,
// grpc client for cache
cacheClient: cacheClient,
jwk: &jose.JSONWebKeySet{},
templates: template.Must(frontend.NewTemplates()),
}

if opts.SigningKey != "" {
decodedCert, err := base64.StdEncoding.DecodeString(opts.SigningKey)
if err != nil {
return nil, fmt.Errorf("authenticate: failed to decode signing key: %w", err)
}
jwk, err := cryptutil.PublicJWKFromBytes(decodedCert, jose.ES256)
if err != nil {
return nil, fmt.Errorf("authenticate: failed to convert jwks: %w", err)
}
a.jwk.Keys = append(a.jwk.Keys, *jwk)
}

templates: template.Must(frontend.NewTemplates()),
}, nil
return a, nil
}
2 changes: 2 additions & 0 deletions authenticate/authenticate_test.go
Expand Up @@ -16,6 +16,8 @@ func newTestOptions(t *testing.T) *config.Options {
opts.Provider = "google"
opts.ClientSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJlMFRxbXJkSXBZWE03c3pSRERWYndXOS83RWJHVWhTdFFJalhsVHNXM1BvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFb0xaRDI2bEdYREhRQmhhZkdlbEVmRDdlNmYzaURjWVJPVjdUbFlIdHF1Y1BFL2hId2dmYQpNY3FBUEZsRmpueUpySXJhYTFlQ2xZRTJ6UktTQk5kNXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="

err := opts.Validate()
if err != nil {
t.Fatal(err)
Expand Down
44 changes: 44 additions & 0 deletions authenticate/handlers.go
Expand Up @@ -36,6 +36,7 @@ func (a *Authenticate) Handler() http.Handler {

// Mount mounts the authenticate routes to the given router.
func (a *Authenticate) Mount(r *mux.Router) {
r.StrictSlash(true)
r.Use(middleware.SetHeaders(httputil.HeadersContentSecurityPolicy))
r.Use(csrf.Protect(
a.cookieSecret,
Expand Down Expand Up @@ -72,12 +73,55 @@ func (a *Authenticate) Mount(r *mux.Router) {
v.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
v.Path("/refresh").Handler(httputil.HandlerFunc(a.Refresh)).Methods(http.MethodGet)

wk := r.PathPrefix("/.well-known/pomerium").Subrouter()
wk.Path("/jwks.json").Handler(httputil.HandlerFunc(a.jwks)).Methods(http.MethodGet)
wk.Path("/").Handler(httputil.HandlerFunc(a.wellKnown)).Methods(http.MethodGet)

// https://www.googleapis.com/oauth2/v3/certs

// programmatic access api endpoint
api := r.PathPrefix("/api").Subrouter()
api.Use(sessions.RetrieveSession(a.sessionLoaders...))
api.Path("/v1/refresh").Handler(httputil.HandlerFunc(a.RefreshAPI))
}

// Well-Known Uniform Resource Identifiers (URIs)
// https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers
func (a *Authenticate) wellKnown(w http.ResponseWriter, r *http.Request) error {
wellKnownURLS := struct {
// URL string referencing the client's JSON Web Key (JWK) Set
// RFC7517 document, which contains the client's public keys.
JSONWebKeySetURL string `json:"jwks_uri"`
OAuth2Callback string `json:"authentication_callback_endpoint"`
ProgrammaticRefreshAPI string `json:"api_refresh_endpoint"`
}{
a.RedirectURL.ResolveReference(&url.URL{Path: "/.well-known/pomerium/jwks.json"}).String(),
a.RedirectURL.ResolveReference(&url.URL{Path: "/oauth2/callback"}).String(),
a.RedirectURL.ResolveReference(&url.URL{Path: "/api/v1/refresh"}).String(),
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Content-Type-Options", "nosniff")
jBytes, err := json.Marshal(wellKnownURLS)
if err != nil {
return err
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s", jBytes)
return nil
}

func (a *Authenticate) jwks(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Content-Type-Options", "nosniff")
jBytes, err := json.Marshal(a.jwk)
if err != nil {
return err
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "%s", jBytes)
return nil
}

// VerifySession is the middleware used to enforce a valid authentication
// session state is attached to the users's request context.
func (a *Authenticate) VerifySession(next http.Handler) http.Handler {
Expand Down

0 comments on commit 3f1faf2

Please sign in to comment.