lib/dam/hydra_dam.go (194 lines of code) (raw):

// Copyright 2019 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 dam import ( "context" "net/http" "net/url" "google.golang.org/grpc/codes" /* copybara-comment */ "google.golang.org/grpc/status" /* copybara-comment */ "golang.org/x/oauth2" /* copybara-comment */ "bitbucket.org/creachadair/stringset" /* copybara-comment */ "github.com/pborman/uuid" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/apis/hydraapi" /* copybara-comment: hydraapi */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/errutil" /* copybara-comment: errutil */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */ "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 */ pb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/dam/v1" /* copybara-comment: go_proto */ ) const ( stateIDInHydra = "state" ) // HydraLogin handles login request from hydra. func (s *Service) HydraLogin(w http.ResponseWriter, r *http.Request) { r.ParseForm() // Use login_challenge fetch information from hydra. challenge, sts := hydra.ExtractLoginChallenge(r) if sts != nil { httputils.WriteError(w, sts.Err()) return } login, err := hydra.GetLoginRequest(s.httpClient, s.hydraAdminURL, challenge) if err != nil { httputils.WriteError(w, err) return } redirect, err := s.hydraLogin(r.Context(), challenge, login) if err != nil { hydra.SendLoginReject(w, r, s.httpClient, s.hydraAdminURL, challenge, err) } else { httputils.WriteRedirect(w, r, redirect) } } // hydraLogin returns redirect and status error func (s *Service) hydraLogin(ctx context.Context, challenge string, login *hydraapi.LoginRequest) (string, error) { u, err := url.Parse(login.RequestURL) if err != nil { return "", errutil.WithErrorReason("url_parse", status.Errorf(codes.FailedPrecondition, "url parse: %v", err)) } in := authHandlerIn{ challenge: challenge, requestedAudience: append(login.RequestedAudience, login.Client.ClientID), requestedScope: login.RequestedScope, clientID: login.Client.ClientID, clientName: login.Client.Name, } // Request tokens for call DAM endpoints, if scope includes "identities". if stringset.Contains(login.RequestedScope, "identities") { in.tokenType = pb.ResourceTokenRequestState_ENDPOINT in.realm = u.Query().Get("realm") if len(in.realm) == 0 { in.realm = storage.DefaultRealm } } else { in.tokenType = pb.ResourceTokenRequestState_DATASET in.ttl, err = extractTTL(u.Query().Get("max_age"), u.Query().Get("ttl")) if err != nil { return "", errutil.WithErrorReason("ttl_invalid", status.Errorf(codes.InvalidArgument, "ttl invalid: %v", err)) } list := u.Query()["resource"] in.resources, err = s.resourceViewRoleFromRequest(list) if err != nil { return "", errutil.WithErrorReason("resource_invalid", status.Errorf(codes.InvalidArgument, "resource invalid: %v", err)) } in.responseKeyFile = u.Query().Get("response_type") == "key-file-type" } out, err := s.auth(ctx, in) if err != nil { return "", err } var opts []oauth2.AuthCodeOption loginHint := u.Query().Get("login_hint") if len(loginHint) != 0 { opt := oauth2.SetAuthURLParam("login_hint", loginHint) opts = append(opts, opt) } auth := out.oauth.AuthCodeURL(out.stateID, opts...) return auth, nil } // HydraConsent handles consent request from hydra. func (s *Service) HydraConsent(w http.ResponseWriter, r *http.Request) { r.ParseForm() // Use consent_challenge fetch information from hydra. challenge, st := hydra.ExtractConsentChallenge(r) if st != nil { httputils.WriteError(w, st.Err()) return } consent, err := hydra.GetConsentRequest(s.httpClient, s.hydraAdminURL, challenge) if err != nil { httputils.WriteError(w, err) return } pageOrRedirect, err := s.hydraConsent(challenge, consent) if err != nil { hydra.SendConsentReject(w, r, s.httpClient, s.hydraAdminURL, challenge, err) } else { pageOrRedirect.writeResp(w, r) } } // hydraConsent returns redirect and status error func (s *Service) hydraConsent(challenge string, consent *hydraapi.ConsentRequest) (_ *htmlPageOrRedirectURL, ferr error) { tx, err := s.store.Tx(true) if err != nil { return nil, status.Errorf(codes.Unavailable, "%v", err) } defer func() { err := tx.Finish() if ferr == nil && err != nil { ferr = status.Errorf(codes.Internal, "%v", err) } }() var stateID string stateID, sts := hydra.ExtractStateIDInConsent(consent) if sts != nil { return nil, sts.Err() } if len(stateID) == 0 { return nil, status.Errorf(codes.FailedPrecondition, "token format invalid: stateID not found") } state := &pb.ResourceTokenRequestState{} err = s.store.ReadTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, tx) if err != nil { return nil, status.Errorf(codes.Unavailable, "read state failed: %v", err) } state.ConsentChallenge = challenge err = s.store.WriteTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, state, nil, tx) if err != nil { return nil, status.Errorf(codes.Unavailable, err.Error()) } if s.skipInformationReleasePage { return s.hydraConsentSkipInformationReleasePage(consent, stateID, state, tx) } return s.hydraConsentRememberConsentOrInformationReleasePage(consent, stateID, state, tx) } func (s *Service) hydraConsentSkipInformationReleasePage(consent *hydraapi.ConsentRequest, stateID string, state *pb.ResourceTokenRequestState, tx storage.Tx) (*htmlPageOrRedirectURL, error) { return s.acceptHydraConsent(stateID, state, tx) } func (s *Service) acceptHydraConsent(stateID string, state *pb.ResourceTokenRequestState, tx storage.Tx) (*htmlPageOrRedirectURL, error) { tokenID := uuid.New() req := &hydraapi.HandledConsentRequest{ GrantedAudience: state.RequestedAudience, GrantedScope: state.RequestedScope, Session: &hydraapi.ConsentRequestSessionData{ AccessToken: map[string]interface{}{ "tid": tokenID, }, IDToken: map[string]interface{}{ "tid": tokenID, }, }, } if state.Type == pb.ResourceTokenRequestState_ENDPOINT { req.Session.AccessToken["identities"] = state.Identities // For endpoint tokens, state is not needed any more. For dataset tokens, state is still needed for exchanging resource token. err := s.store.DeleteTx(storage.ResourceTokenRequestStateDataType, storage.DefaultRealm, storage.DefaultUser, stateID, storage.LatestRev, tx) if err != nil { return nil, status.Errorf(codes.Unavailable, "delete state failed: %v", err) } } else { req.Session.AccessToken["cart"] = stateID } resp, err := hydra.AcceptConsent(s.httpClient, s.hydraAdminURL, state.ConsentChallenge, req) if err != nil { return nil, err } return &htmlPageOrRedirectURL{redirect: resp.RedirectTo}, nil } func (s *Service) extractCartFromAccessToken(id *ga4gh.Identity) (string, error) { v, ok := id.Extra["cart"] if !ok { return "", status.Errorf(codes.Unauthenticated, "token does not have 'cart' claim") } cart, ok := v.(string) if !ok { return "", status.Errorf(codes.Internal, "token 'cart' claim have unwanted type") } if len(cart) == 0 { return "", status.Errorf(codes.Unauthenticated, "token has empty 'cart' claim") } return cart, nil } type htmlPageOrRedirectURL struct { page string redirect string } func (h *htmlPageOrRedirectURL) writeResp(w http.ResponseWriter, r *http.Request) { if len(h.page) > 0 { httputils.WriteHTMLResp(w, h.page, nil) } else { httputils.WriteRedirect(w, r, h.redirect) } }