lib/hydra/hydra.go (241 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 hydra contains helpers for using hydra package hydra import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "strings" "google.golang.org/grpc/codes" /* copybara-comment */ "google.golang.org/grpc/status" /* 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/httputils" /* copybara-comment: httputils */ glog "github.com/golang/glog" /* copybara-comment */ ) // GetLoginRequest fetches information on a login request. func GetLoginRequest(client *http.Client, hydraAdminURL, challenge string) (*hydraapi.LoginRequest, error) { u := getURL(hydraAdminURL, "login", url.QueryEscape(challenge)) resp := &hydraapi.LoginRequest{} err := httpGet(client, u, resp) return resp, err } // AcceptLogin tells hydra to accept a login request. func AcceptLogin(client *http.Client, hydraAdminURL, challenge string, r *hydraapi.HandledLoginRequest) (*hydraapi.RequestHandlerResponse, error) { u := putURL(hydraAdminURL, "login", "accept", url.QueryEscape(challenge)) resp := &hydraapi.RequestHandlerResponse{} err := httpPut(client, u, r, resp) return resp, err } // RejectLogin tells hydra to reject a login request. func RejectLogin(client *http.Client, hydraAdminURL, challenge string, r *hydraapi.RequestDeniedError) (*hydraapi.RequestHandlerResponse, error) { u := putURL(hydraAdminURL, "login", "reject", url.QueryEscape(challenge)) resp := &hydraapi.RequestHandlerResponse{} err := httpPut(client, u, r, resp) return resp, err } // GetConsentRequest fetches information on a consent request. func GetConsentRequest(client *http.Client, hydraAdminURL, challenge string) (*hydraapi.ConsentRequest, error) { u := getURL(hydraAdminURL, "consent", url.QueryEscape(challenge)) resp := &hydraapi.ConsentRequest{} err := httpGet(client, u, resp) return resp, err } // AcceptConsent tells hydra to accept a consent request. func AcceptConsent(client *http.Client, hydraAdminURL, challenge string, r *hydraapi.HandledConsentRequest) (*hydraapi.RequestHandlerResponse, error) { u := putURL(hydraAdminURL, "consent", "accept", url.QueryEscape(challenge)) resp := &hydraapi.RequestHandlerResponse{} err := httpPut(client, u, r, resp) return resp, err } // RejectConsent tells hydra to rejects a consent request. func RejectConsent(client *http.Client, hydraAdminURL, challenge string, r *hydraapi.RequestDeniedError) (*hydraapi.RequestHandlerResponse, error) { u := putURL(hydraAdminURL, "consent", "reject", url.QueryEscape(challenge)) resp := &hydraapi.RequestHandlerResponse{} err := httpPut(client, u, r, resp) return resp, err } // ListClients list all OAuth clients in hydra. func ListClients(client *http.Client, hydraAdminURL string) ([]*hydraapi.Client, error) { u := hydraAdminURL + "/clients" resp := []*hydraapi.Client{} err := httpGet(client, u, &resp) return resp, err } // CreateClient creates OAuth client in hydra. func CreateClient(client *http.Client, hydraAdminURL string, oauthClient *hydraapi.Client) (*hydraapi.Client, error) { u := hydraAdminURL + "/clients" resp := &hydraapi.Client{} err := httpPostJSON(client, u, oauthClient, resp) return resp, err } // GetClient gets an OAUth 2.0 client by its ID. func GetClient(client *http.Client, hydraAdminURL, id string) (*hydraapi.Client, error) { u := hydraAdminURL + "/clients/" + id resp := &hydraapi.Client{} err := httpGet(client, u, resp) return resp, err } // UpdateClient updates an existing OAuth 2.0 Client. func UpdateClient(client *http.Client, hydraAdminURL, id string, oauthClient *hydraapi.Client) (*hydraapi.Client, error) { u := hydraAdminURL + "/clients/" + id resp := &hydraapi.Client{} err := httpPut(client, u, oauthClient, resp) return resp, err } // DeleteClient delete an existing OAuth 2.0 Client by its ID. func DeleteClient(client *http.Client, hydraAdminURL, id string) error { u := hydraAdminURL + "/clients/" + id err := httpDelete(client, u) return err } // Introspect token, validate the given token and return token claims. func Introspect(client *http.Client, hydraAdminURL, token string) (*hydraapi.Introspection, error) { u := hydraAdminURL + "/oauth2/introspect" data := url.Values{} data.Set("token", url.QueryEscape(token)) response := &hydraapi.Introspection{} req, err := http.NewRequest(http.MethodPost, u, strings.NewReader(data.Encode())) if err != nil { return nil, status.Errorf(codes.Internal, "http.NewRequest for Introspect failed to initialize: %v", err) } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return nil, status.Errorf(codes.Unavailable, "Introspect to hydra failed: %v", err) } defer resp.Body.Close() if err := httpResponse(resp, response); err != nil { return nil, err } return response, nil } // ListConsents lists all consents of user (subject). func ListConsents(client *http.Client, hydraAdminURL, subject string) ([]*hydraapi.PreviousConsentSession, error) { // TODO: consider support page param. u := hydraAdminURL + "/oauth2/auth/sessions/consent?subject=" + subject resp := []*hydraapi.PreviousConsentSession{} if err := httpGet(client, u, &resp); err != nil { return nil, err } return resp, nil } // RevokeConsents revokes all consents of user for a client. clientID is optional, will revoke all consent of user if no clientID. func RevokeConsents(client *http.Client, hydraAdminURL, subject, clientID string) error { u, err := url.Parse(hydraAdminURL + "/oauth2/auth/sessions/consent") if err != nil { return err } q := url.Values{} q.Add("subject", subject) if len(clientID) != 0 { q.Add("client", clientID) } u.RawQuery = q.Encode() return httpDelete(client, u.String()) } // RevokeToken revokes the given token, token maybe refresh token or access token. func RevokeToken(client *http.Client, hydraAdminURL, token string) error { u, err := url.Parse(hydraAdminURL + "/oauth2/revoke") if err != nil { return status.Errorf(codes.Internal, "invalid hydraAdminURL: %v", err) } q := url.Values{} q.Add("token", token) req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(q.Encode())) if err != nil { return status.Errorf(codes.Internal, "http.NewRequest for RevokeToken failed to initialize: %v", err) } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return status.Errorf(codes.Unavailable, "RevokeToken to hydra failed: %v", err) } defer resp.Body.Close() // Doc shows API may return empty body for success response. // https://www.ory.sh/docs/next/hydra/sdk/api#revoke-oauth2-tokens if httputils.IsHTTPSuccess(resp.StatusCode) { return nil } return httpResponse(resp, nil) } func getURL(hydraAdminURL, flow, challenge string) string { const getURLPattern = "%s/oauth2/auth/requests/%s?%s_challenge=%s" return fmt.Sprintf(getURLPattern, hydraAdminURL, flow, flow, url.QueryEscape(challenge)) } func putURL(hydraAdminURL, flow, action, challenge string) string { const putURLPattern = "%s/oauth2/auth/requests/%s/%s?%s_challenge=%s" return fmt.Sprintf(putURLPattern, hydraAdminURL, flow, action, flow, url.QueryEscape(challenge)) } func httpResponse(resp *http.Response, response interface{}) error { if httputils.IsHTTPError(resp.StatusCode) { gErr := &hydraapi.GenericError{} if err := httputils.DecodeJSON(resp.Body, gErr); err != nil { return status.Errorf(codes.Internal, "DecodeJSON() failed: %v", err) } // TODO: figure out what error from hydra should handle. glog.Errorf("error from hydra: %v, debug info: %s", gErr, gErr.Debug) return toStatusErr(gErr) } return httputils.DecodeJSON(resp.Body, response) } func toStatusErr(err *hydraapi.GenericError) error { reason := "" if err.Name != nil { reason = *err.Name } return errutil.WithErrorReason( reason, status.Errorf(httputils.RPCCode(int(err.Code)), err.Description), ) } func httpPut(client *http.Client, url string, request interface{}, response interface{}) error { body, err := json.Marshal(request) if err != nil { return err } req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body)) if err != nil { return status.Errorf(codes.Internal, "http.NewRequest for httpPut failed to initialize: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return status.Errorf(codes.Unavailable, "httpPut to hydra failed: %v", err) } defer resp.Body.Close() return httpResponse(resp, response) } func httpPostJSON(client *http.Client, url string, request interface{}, response interface{}) error { body, err := json.Marshal(request) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return status.Errorf(codes.Internal, "http.NewRequest for httpPostJSON failed to initialize: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return status.Errorf(codes.Unavailable, "httpPostJSON to hydra failed: %v", err) } defer resp.Body.Close() return httpResponse(resp, response) } func httpDelete(client *http.Client, url string) error { req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return status.Errorf(codes.Internal, "http.NewRequest for httpDelete failed to initialize: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return status.Errorf(codes.Unavailable, "httpDelete to hydra failed: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNoContent { return nil } return httpResponse(resp, nil) } func httpGet(client *http.Client, url string, response interface{}) error { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return status.Errorf(codes.Internal, "http.NewRequest for httpGet failed to initialize: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") resp, err := client.Do(req) if err != nil { return status.Errorf(codes.Unavailable, "httpGet to hydra failed: %v", err) } defer resp.Body.Close() return httpResponse(resp, response) }