feat: initial commit

This commit is contained in:
2023-11-15 20:38:25 +01:00
commit e199fe3d26
67 changed files with 4152 additions and 0 deletions

263
internal/hydra/client.go Normal file
View File

@ -0,0 +1,263 @@
package hydra
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"time"
"github.com/pkg/errors"
)
type Client struct {
baseURL *url.URL
http *http.Client
}
func (c *Client) LoginRequest(challenge string) (*LoginResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/login", url.Values{
"login_challenge": []string{challenge},
})
res, err := c.http.Get(u)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve login response")
}
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return nil, errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
}
defer res.Body.Close()
decoder := json.NewDecoder(res.Body)
loginRes := &LoginResponse{}
if err := decoder.Decode(loginRes); err != nil {
return nil, errors.Wrap(err, "could not decode json response")
}
return loginRes, nil
}
func (c *Client) AcceptLoginRequest(challenge string, req *AcceptLoginRequest) (*AcceptResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/login/accept", url.Values{
"login_challenge": []string{challenge},
})
res := &AcceptResponse{}
if err := c.putJSON(u, req, res); err != nil {
return nil, err
}
return res, nil
}
func (c *Client) RejectLoginRequest(challenge string, req *RejectRequest) (*RejectResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/login/reject", url.Values{
"login_challenge": []string{challenge},
})
res := &RejectResponse{}
if err := c.putJSON(u, req, res); err != nil {
return nil, err
}
return res, nil
}
func (c *Client) LogoutRequest(challenge string) (*LogoutResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/logout", url.Values{
"logout_challenge": []string{challenge},
})
res, err := c.http.Get(u)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve logout response")
}
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return nil, errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
}
defer res.Body.Close()
decoder := json.NewDecoder(res.Body)
logoutRes := &LogoutResponse{}
if err := decoder.Decode(logoutRes); err != nil {
return nil, errors.Wrap(err, "could not decode json response")
}
return logoutRes, nil
}
func (c *Client) AcceptLogoutRequest(challenge string) (*AcceptResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/logout/accept", url.Values{
"logout_challenge": []string{challenge},
})
res := &AcceptResponse{}
if err := c.putJSON(u, nil, res); err != nil {
return nil, err
}
return res, nil
}
func (c *Client) RejectLogoutRequest(challenge string, req *RejectRequest) (*RejectResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/logout/reject", url.Values{
"logout_challenge": []string{challenge},
})
res := &RejectResponse{}
if err := c.putJSON(u, req, res); err != nil {
return nil, err
}
return res, nil
}
func (c *Client) ConsentRequest(challenge string) (*ConsentResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/consent", url.Values{
"consent_challenge": []string{challenge},
})
res, err := c.http.Get(u)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve login response")
}
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return nil, errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
}
defer res.Body.Close()
decoder := json.NewDecoder(res.Body)
consentRes := &ConsentResponse{}
if err := decoder.Decode(consentRes); err != nil {
return nil, errors.Wrap(err, "could not decode json response")
}
return consentRes, nil
}
func (c *Client) AcceptConsentRequest(challenge string, req *AcceptConsentRequest) (*AcceptResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/consent/accept", url.Values{
"consent_challenge": []string{challenge},
})
res := &AcceptResponse{}
if err := c.putJSON(u, req, res); err != nil {
return nil, err
}
return res, nil
}
func (c *Client) RejectConsentRequest(challenge string, req *RejectRequest) (*RejectResponse, error) {
u := fromURL(*c.baseURL, "/oauth2/auth/requests/consent/reject", url.Values{
"consent_challenge": []string{challenge},
})
res := &RejectResponse{}
if err := c.putJSON(u, req, res); err != nil {
return nil, err
}
return res, nil
}
func (c *Client) LoginChallenge(r *http.Request) (string, error) {
return c.challenge(r, "login_challenge")
}
func (c *Client) ConsentChallenge(r *http.Request) (string, error) {
return c.challenge(r, "consent_challenge")
}
func (c *Client) LogoutChallenge(r *http.Request) (string, error) {
return c.challenge(r, "logout_challenge")
}
func (c *Client) challenge(r *http.Request, name string) (string, error) {
challenge := r.URL.Query().Get(name)
if challenge == "" {
return "", ErrChallengeNotFound
}
return challenge, nil
}
func (c *Client) putJSON(u string, payload interface{}, result interface{}) error {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
if err := encoder.Encode(payload); err != nil {
return errors.Wrap(err, "could not encode request body")
}
req, err := http.NewRequest("PUT", u, &buf)
if err != nil {
return errors.Wrap(err, "could not create request")
}
res, err := c.http.Do(req)
if err != nil {
return errors.Wrap(err, "could not retrieve login response")
}
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
}
defer res.Body.Close()
decoder := json.NewDecoder(res.Body)
if err := decoder.Decode(result); err != nil {
return errors.Wrap(err, "could not decode json response")
}
return nil
}
func fromURL(url url.URL, path string, query url.Values) string {
url.Path = path
url.RawQuery = query.Encode()
return url.String()
}
type fakeSSLTerminationTransport struct {
T http.RoundTripper
}
func (t *fakeSSLTerminationTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Add("X-Forwarded-Proto", "https")
return t.T.RoundTrip(req)
}
func NewClient(baseURL *url.URL, fakeSSLTermination bool, httpTimeout time.Duration) *Client {
httpClient := &http.Client{
Timeout: httpTimeout,
}
if fakeSSLTermination {
httpClient.Transport = &fakeSSLTerminationTransport{http.DefaultTransport}
}
return &Client{
baseURL: baseURL,
http: httpClient,
}
}

8
internal/hydra/error.go Normal file
View File

@ -0,0 +1,8 @@
package hydra
import "errors"
var (
ErrUnexpectedHydraResponse = errors.New("unexpected hydra response")
ErrChallengeNotFound = errors.New("challenge not found")
)

View File

@ -0,0 +1,30 @@
package hydra
import (
"net/url"
"time"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
func ServiceProvider(rawBaseURL string, fakeSSLTermination bool, httpTimeout time.Duration) service.Provider {
var (
baseURL *url.URL
err error
)
baseURL, err = url.Parse(rawBaseURL)
if err != nil {
err = errors.Wrap(err, "could not parse base url")
}
client := NewClient(baseURL, fakeSSLTermination, httpTimeout)
return func(ctn *service.Container) (interface{}, error) {
if err != nil {
return nil, err
}
return client, nil
}
}

29
internal/hydra/request.go Normal file
View File

@ -0,0 +1,29 @@
package hydra
type AcceptLoginRequest struct {
Subject string `json:"subject"`
Remember bool `json:"remember"`
RememberFor int `json:"remember_for"`
ACR string `json:"acr"`
Context map[string]interface{} `json:"context"`
}
type AcceptLogoutRequest struct{}
type AcceptConsentRequest struct {
GrantScope []string `json:"grant_scope"`
GrantAccessTokenAudience []string `json:"grant_access_token_audience"`
Remember bool `json:"remember"`
RememberFor int `json:"remember_for"`
Session AcceptConsentSession `json:"session"`
}
type AcceptConsentSession struct {
AccessToken map[string]interface{} `json:"access_token"`
IDToken map[string]interface{} `json:"id_token"`
}
type RejectRequest struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}

View File

@ -0,0 +1,88 @@
package hydra
import "time"
// https://www.ory.sh/hydra/docs/reference/api#get-a-login-request
type ClientResponseFragment struct {
AllowCORSOrigins []string `json:"allowed_cors_origins"`
Audience []string `json:"audience"`
BackChannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"`
BackChannelLogoutURI string `json:"backchannel_logout_uri"`
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
ClientSecret string `json:"client_secret"`
ClientSecretExpiresAt int `json:"client_secret_expires_at"`
ClientURI string `json:"client_uri"`
Contacts []string `json:"contacts"`
CreatedAt time.Time `json:"created_at"`
FrontChannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"`
FrontChannelLogoutURL string `json:"frontchannel_logout_uri"`
GrantTypes []string `json:"grant_types"`
JWKS map[string]interface{} `json:"jwks"`
JwksURI string `json:"jwks_uri"`
LogoURI string `json:"logo_uri"`
Metadata map[string]interface{} `json:"metadata"`
Owner string `json:"owner"`
PolicyURI string `json:"policy_uri"`
PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris"`
RedirectURIs []string `json:"redirect_uris"`
RequestObjectSigningAlg string `json:"request_object_signing_alg"`
RequestURIs []string `json:"request_uris"`
ResponseTypes []string `json:"response_types"`
Scope string `json:"scope"`
SectorIdentifierURI string `json:"sector_identifier_uri"`
SubjectType string `json:"subject_type"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
TosURI string `json:"tos_uri"`
UpdatedAt time.Time `json:"updated_at"`
UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
}
type OidcContextResponseFragment struct {
ACRValues []string `json:"acr_values"`
Display string `json:"display"`
IDTokenHintClaims map[string]interface{} `json:"id_token_hint_claims"`
LoginHint string `json:"login_hint"`
UILocales []string `json:"ui_locales"`
}
type LoginResponse struct {
Challenge string `json:"challenge"`
Skip bool `json:"skip"`
Subject string `json:"subject"`
Client ClientResponseFragment `json:"client"`
RequestURL string `json:"request_url"`
RequestedScope []string `json:"requested_scope"`
OidcContext OidcContextResponseFragment `json:"oidc_context"`
RequestedAccessTokenAudience []string `json:"requested_access_token_audience"`
SessionID string `json:"session_id"`
}
type AcceptResponse struct {
RedirectTo string `json:"redirect_to"`
}
type RejectResponse struct {
RedirectTo string `json:"redirect_to"`
}
type LogoutResponse struct {
Subject string `json:"subject"`
SessionID string `json:"sid"`
RPInitiated bool `json:"rp_initiated"`
RequestURL string `json:"request_url"`
}
type ConsentResponse struct {
Challenge string `json:"challenge"`
Skip bool `json:"skip"`
Subject string `json:"subject"`
Client ClientResponseFragment `json:"client"`
RequestURL string `json:"request_url"`
RequestedScope []string `json:"requested_scope"`
OidcContext OidcContextResponseFragment `json:"oidc_context"`
RequestedAccessTokenAudience []string `json:"requested_access_token_audience"`
SessionID string `json:"session_id"`
Context map[string]interface{} `json:"context"`
}

33
internal/hydra/service.go Normal file
View File

@ -0,0 +1,33 @@
package hydra
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "hydra"
// From retrieves the hydra service in the given container
func From(container *service.Container) (*Client, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Client)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the hydra service in the given container or panic otherwise
func Must(container *service.Container) *Client {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}