lib/hydraproxy/proxy.go (91 lines of code) (raw):

// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package hydraproxy contains a hydra proxy service to proxy request to hydra if needed. package hydraproxy import ( "bytes" "fmt" "io/ioutil" "net/http" "net/http/httputil" "net/url" "google.golang.org/grpc/codes" /* copybara-comment */ "google.golang.org/grpc/status" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/httputils" /* copybara-comment: httputils */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/hydra" /* copybara-comment: hydra */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */ tpb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/store/tokens" /* copybara-comment: go_proto */ ) // Service is the service proxy the request to hydra. type Service struct { httpClient *http.Client hydraAdminURL string hydraPublicURLProxy *httputil.ReverseProxy store storage.Store } // New creates the hydra proxy service. func New(client *http.Client, hydraAdminURL, hydraPublicURLInternal string, store storage.Store) (*Service, error) { s := &Service{ httpClient: client, hydraAdminURL: hydraAdminURL, store: store, } u, err := url.Parse(hydraPublicURLInternal) if err != nil { return nil, fmt.Errorf("url.Parse(%s): %v", hydraPublicURLInternal, err) } s.hydraPublicURLProxy = httputil.NewSingleHostReverseProxy(u) return s, nil } // HydraOAuthToken proxy the POST /oauth2/token request. // - for code exhange token: do nothing, just proxy. // - for refresh token exchange token: check the token is not revoked before proxy the request. // TODO: should unify with HydraOAuthToken in DAM. func (s *Service) HydraOAuthToken(w http.ResponseWriter, r *http.Request) { r.ParseForm() // introspect the refresh token before proxy the request to exchange. if isRefreshTokenExchange(r) { deleted, err := s.tokenDeleted(r.PostFormValue("refresh_token")) if err != nil { httputils.WriteError(w, err) return } if deleted { httputils.WriteError(w, status.Errorf(codes.Unauthenticated, "token revoked")) return } } // Encode the form back into request body r.Body = ioutil.NopCloser(bytes.NewBufferString(r.PostForm.Encode())) s.hydraPublicURLProxy.ServeHTTP(w, r) } func isRefreshTokenExchange(r *http.Request) bool { return r.PostFormValue("grant_type") == "refresh_token" } // tokenRevoked checks if token is in pending delete state, if it is in pending delete state, revoke it from hydra. // Returns deleted and error func (s *Service) tokenDeleted(refreshToken string) (_ bool, ferr error) { if len(refreshToken) == 0 { return false, status.Error(codes.FailedPrecondition, "no refresh_token") } in, err := hydra.Introspect(s.httpClient, s.hydraAdminURL, refreshToken) if err != nil { return false, err } tid, err := hydra.ExtractTokenIDInIntrospect(in) if err != nil { return false, err } tx, err := s.store.Tx(true) if err != nil { return false, status.Errorf(codes.Unavailable, "tokenDeleted: can not get tx: %v", err) } defer func() { err := tx.Finish() if ferr == nil { ferr = err } }() pending := &tpb.PendingDeleteToken{} err = s.store.ReadTx(storage.PendingDeleteTokenDatatype, storage.DefaultRealm, in.Subject, tid, storage.LatestRev, pending, tx) if err != nil { // No pending delete for this token. if storage.ErrNotFound(err) { return false, nil } return false, status.Errorf(codes.Unavailable, "tokenDeleted: read PendingDeleteToken failed: %v", err) } // delete this token if err := hydra.RevokeToken(s.httpClient, s.hydraAdminURL, refreshToken); err != nil { return false, err } if err := s.store.DeleteTx(storage.PendingDeleteTokenDatatype, storage.DefaultRealm, in.Subject, tid, storage.LatestRev, tx); err != nil { return false, status.Errorf(codes.Unavailable, "tokenDeleted: delete PendingDeleteToken failed: %v", err) } return true, err }