Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

authenticate: add jwks and .well-known endpoint #745

Merged
merged 4 commits into from May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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