From bc15b8a396ec3bdfc9785889b9f2f30111acaeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 12 Apr 2023 15:18:26 +0200 Subject: [PATCH] work on logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- ocis-pkg/oidc/client.go | 87 ++++++++++++++- ocis-pkg/oidc/logoutverify.go | 101 +---------------- ocis-pkg/oidc/metadata.go | 76 +++++++++++++ services/proxy/pkg/command/server.go | 121 +++++++++++++-------- services/proxy/pkg/middleware/oidc_auth.go | 10 +- 5 files changed, 241 insertions(+), 154 deletions(-) diff --git a/ocis-pkg/oidc/client.go b/ocis-pkg/oidc/client.go index 607339070..ea415166c 100644 --- a/ocis-pkg/oidc/client.go +++ b/ocis-pkg/oidc/client.go @@ -1,6 +1,7 @@ package oidc import ( + "bytes" "context" "encoding/json" "errors" @@ -18,12 +19,14 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/proxy/pkg/config" "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" ) // OIDCProvider used to mock the oidc provider during tests type OIDCProvider interface { UserInfo(ctx context.Context, ts oauth2.TokenSource) (*UserInfo, error) VerifyAccessToken(ctx context.Context, token string) (jwt.RegisteredClaims, []string, error) + VerifyLogoutToken(ctx context.Context, token string) (*LogoutToken, error) } // KeySet is a set of publc JSON Web Keys that can be used to validate the signature @@ -279,13 +282,95 @@ func (c *oidcClient) VerifyAccessToken(ctx context.Context, token string) (jwt.R } } +func (c *oidcClient) VerifyLogoutToken(ctx context.Context, rawIDToken string) (*LogoutToken, error) { + jws, err := jose.ParseSigned(rawIDToken) + if err != nil { + return nil, err + } + // Throw out tokens with invalid claims before trying to verify the token. This lets + // us do cheap checks before possibly re-syncing keys. + payload, err := parseJWT(rawIDToken) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt: %v", err) + } + var token LogoutToken + if err := json.Unmarshal(payload, &token); err != nil { + return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) + } + + //4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both. + if token.Subject == "" && token.SessionId == "" { + return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both") + } + //5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout. + if token.Events.Event == nil { + return nil, fmt.Errorf("oidc: logout token must contain logout event") + } + //6. Verify that the Logout Token does not contain a nonce Claim. + type nonce struct { + Nonce *string `json:"nonce"` + } + var n nonce + json.Unmarshal(payload, &n) + if n.Nonce != nil { + return nil, fmt.Errorf("oidc: nonce on logout token MUST NOT be present") + } + // Check issuer. + if !v.config.SkipIssuerCheck && token.Issuer != v.issuer { + return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, token.Issuer) + } + + // If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty. + // + // This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party). + if !v.config.SkipClientIDCheck { + if v.config.ClientID != "" { + if !contains(token.Audience, v.config.ClientID) { + return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, token.Audience) + } + } else { + return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set") + } + } + + switch len(jws.Signatures) { + case 0: + return nil, fmt.Errorf("oidc: id token not signed") + case 1: + default: + return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") + } + + sig := jws.Signatures[0] + supportedSigAlgs := v.config.SupportedSigningAlgs + if len(supportedSigAlgs) == 0 { + supportedSigAlgs = []string{gOidc.RS256} + } + + if !contains(supportedSigAlgs, sig.Header.Algorithm) { + return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm) + } + + gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("failed to verify signature: %v", err) + } + + // Ensure that the payload returned by the square actually matches the payload parsed earlier. + if !bytes.Equal(gotPayload, payload) { + return nil, errors.New("oidc: internal error, payload parsed did not match previous payload") + } + + return &token, nil +} + // verifyAccessTokenJWT tries to parse and verify the access token as a JWT. func (c *oidcClient) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, []string, error) { var claims jwt.RegisteredClaims var mapClaims []string jwks := c.getKeyfunc() if jwks == nil { - return claims, mapClaims, errors.New("Error initializing jwks keyfunc") + return claims, mapClaims, errors.New("error initializing jwks keyfunc") } _, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc) diff --git a/ocis-pkg/oidc/logoutverify.go b/ocis-pkg/oidc/logoutverify.go index ba0db4295..67041e8c2 100644 --- a/ocis-pkg/oidc/logoutverify.go +++ b/ocis-pkg/oidc/logoutverify.go @@ -11,97 +11,11 @@ import ( "gopkg.in/square/go-jose.v2" - "time" - gOidc "github.com/coreos/go-oidc/v3/oidc" ) // This adds the ability to verify Logout Tokens as specified in https://openid.net/specs/openid-connect-backchannel-1_0.html -type logoutEvent struct { - Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"` -} - -type audience []string - -func (a *audience) UnmarshalJSON(b []byte) error { - var s string - if json.Unmarshal(b, &s) == nil { - *a = audience{s} - return nil - } - var auds []string - if err := json.Unmarshal(b, &auds); err != nil { - return err - } - *a = auds - return nil -} - -type jsonTime time.Time - -func (j *jsonTime) UnmarshalJSON(b []byte) error { - var n json.Number - if err := json.Unmarshal(b, &n); err != nil { - return err - } - var unix int64 - - if t, err := n.Int64(); err == nil { - unix = t - } else { - f, err := n.Float64() - if err != nil { - return err - } - unix = int64(f) - } - *j = jsonTime(time.Unix(unix, 0)) - return nil -} - -// logoutToken -type logoutToken struct { - Issuer string `json:"iss"` - Subject string `json:"sub"` - Audience audience `json:"aud"` - IssuedAt jsonTime `json:"iat"` - JwtID string `json:"jti"` - Events logoutEvent `json:"events"` - Sid string `json:"sid"` -} - -// Logout Token -type LogoutToken struct { - // The URL of the server which issued this token. OpenID Connect - // requires this value always be identical to the URL used for - // initial discovery. - // - // Note: Because of a known issue with Google Accounts' implementation - // this value may differ when using Google. - // - // See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo - Issuer string - - // A unique string which identifies the end user. - Subject string - - // The client ID, or set of client IDs, that this token is issued for. For - // common uses, this is the client that initialized the auth flow. - // - // This package ensures the audience contains an expected value. - Audience []string - - // When the token was issued by the provider. - IssuedAt time.Time - - // The Session Id - SessionId string - - // Jwt Id - JwtID string -} - // LogoutTokenVerifier provides verification for Logout Tokens. type LogoutTokenVerifier struct { keySet gOidc.KeySet @@ -136,13 +50,13 @@ func (v *LogoutTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*L if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %v", err) } - var token logoutToken + var token LogoutToken if err := json.Unmarshal(payload, &token); err != nil { return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) } //4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both. - if token.Subject == "" && token.Sid == "" { + if token.Subject == "" && token.SessionId == "" { return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both") } //5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout. @@ -204,16 +118,7 @@ func (v *LogoutTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*L return nil, errors.New("oidc: internal error, payload parsed did not match previous payload") } - t := &LogoutToken{ - Issuer: token.Issuer, - Subject: token.Subject, - Audience: token.Audience, - IssuedAt: time.Time(token.IssuedAt), - SessionId: token.Sid, - JwtID: token.JwtID, - } - - return t, nil + return &token, nil } func parseJWT(p string) ([]byte, error) { diff --git a/ocis-pkg/oidc/metadata.go b/ocis-pkg/oidc/metadata.go index bcbecaf82..b5eec20fb 100644 --- a/ocis-pkg/oidc/metadata.go +++ b/ocis-pkg/oidc/metadata.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "strings" + "time" "github.com/owncloud/ocis/v2/ocis-pkg/log" ) @@ -53,6 +54,81 @@ type ProviderMetadata struct { //claim_types_supported } +// Logout Token +type LogoutToken struct { + // The URL of the server which issued this token. OpenID Connect + // requires this value always be identical to the URL used for + // initial discovery. + // + // Note: Because of a known issue with Google Accounts' implementation + // this value may differ when using Google. + // + // See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo + Issuer string `json:iss` // example "https://server.example.com" + + // A unique string which identifies the end user. + Subject string `json:sub` //"248289761001" + + // The client ID, or set of client IDs, that this token is issued for. For + // common uses, this is the client that initialized the auth flow. + // + // This package ensures the audience contains an expected value. + Audience audience `json:aud` // "s6BhdRkqt3" + + // When the token was issued by the provider. + IssuedAt jsonTime `json:"iat"` + + // The Session Id + SessionId string `json:"sid"` + + Events logoutEvent `json:"events"` + + // Jwt Id + JwtID string `json:"jti"` +} + +type logoutEvent struct { + Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"` +} + +type audience []string + +func (a *audience) UnmarshalJSON(b []byte) error { + var s string + if json.Unmarshal(b, &s) == nil { + *a = audience{s} + return nil + } + var auds []string + if err := json.Unmarshal(b, &auds); err != nil { + return err + } + *a = auds + return nil +} + +type jsonTime time.Time + +func (j *jsonTime) UnmarshalJSON(b []byte) error { + var n json.Number + if err := json.Unmarshal(b, &n); err != nil { + return err + } + var unix int64 + + if t, err := n.Int64(); err == nil { + unix = t + } else { + f, err := n.Float64() + if err != nil { + return err + } + unix = int64(f) + } + *j = jsonTime(time.Unix(unix, 0)) + return nil +} + func GetIDPMetadata(logger log.Logger, client *http.Client, idpURI string) (ProviderMetadata, error) { wellknownURI := strings.TrimSuffix(idpURI, "/") + wellknownPath diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index 86ae967cf..83f653ee7 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -3,7 +3,9 @@ package command import ( "context" "crypto/tls" + "errors" "fmt" + "io" "net/http" "time" @@ -40,20 +42,14 @@ import ( microstore "go-micro.dev/v4/store" ) -type LogoutHandler struct { - cache microstore.Store - logger log.Logger - config config.Config -} - -type LogoutToken struct { - iss string `json:iss` // example "https://server.example.com" - sub int64 `json:sub` //"248289761001" - aud string `json:aud` // "s6BhdRkqt3" - iat int64 `json:iat` // 1471566154 - jti string `json:jti` // "bWJq" - sid string `json:sid` // "08a5019c-17e1-4977-8f42-65a12843ea02" - events map[string][]string `json:events` // {"http://schemas.openid.net/event/backchannel-logout": {}} +type StaticRouteHandler struct { + prefix string + proxy http.Handler + sidCache microstore.Store + accessTokenCache microstore.Store + logger log.Logger + config config.Config + oidcClient oidc.OIDCProvider } // Server is the entrypoint for the server command. @@ -85,6 +81,25 @@ func Server(cfg *config.Config) *cli.Command { return err } + var oidcHTTPClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec + }, + DisableKeepAlives: true, + }, + Timeout: time.Second * 10, + } + + oidcClient := oidc.NewOIDCClient( + oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod), + oidc.WithLogger(logger), + oidc.WithHTTPClient(oidcHTTPClient), + oidc.WithOidcIssuer(cfg.OIDC.Issuer), + oidc.WithJWKSOptions(cfg.OIDC.JWKS), + ) + var ( m = metrics.New() ) @@ -105,14 +120,24 @@ func Server(cfg *config.Config) *cli.Command { proxy.Logger(logger), proxy.Config(cfg), ) + + lh := StaticRouteHandler{ + prefix: cfg.HTTP.Root, + sidCache: cache, // FIXME use correct cache + accessTokenCache: cache, // FIXME use correct cache + logger: logger, + config: *cfg, + oidcClient: oidcClient, + proxy: rp, + } if err != nil { - return fmt.Errorf("Failed to initialize reverse proxy: %w", err) + return fmt.Errorf("failed to initialize reverse proxy: %w", err) } { middlewares := loadMiddlewares(ctx, logger, cfg, cache) server, err := proxyHTTP.Server( - proxyHTTP.Handler(handlePredefinedRoutes(cfg, logger, rp, cache, middlewares)), + proxyHTTP.Handler(lh.handler()), proxyHTTP.Logger(logger), proxyHTTP.Context(ctx), proxyHTTP.Config(cfg), @@ -164,49 +189,53 @@ func Server(cfg *config.Config) *cli.Command { } } -func handlePredefinedRoutes(cfg *config.Config, logger log.Logger, handler http.Handler, cache microstore.Store, middlewares alice.Chain) http.Handler { +func (h *StaticRouteHandler) handler() http.Handler { m := chi.NewMux() var methods = []string{"PROPFIND", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK"} for _, k := range methods { chi.RegisterMethod(k) } - p := LogoutHandler{ - cache: cache, - logger: logger, - config: *cfg, - } - - m.Route(cfg.HTTP.Root, func(r chi.Router) { + m.Route(h.prefix, func(r chi.Router) { // Wrapper for backchannel logout - // TODO: remove the GET for the backchannel_logout - r.Get("/backchannel_logout", p.backchannelLogout) - r.Post("/backchannel_logout", p.backchannelLogout) + r.Post("/backchannel_logout", h.backchannelLogout) + // TODO: migrate oidc well knowns here in a second wrapper - r.HandleFunc("/*", handler.ServeHTTP) + r.HandleFunc("/*", h.proxy.ServeHTTP) + }) return m } -func (p *LogoutHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) { - /* - var oidcHTTPClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: p.config.OIDC.Insecure, //nolint:gosec - }, - DisableKeepAlives: true, - }, - Timeout: time.Second * 10, +func (h *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) { + + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + render.Status(r, http.StatusBadRequest) + return + } + + logoutToken, err := h.oidcClient.VerifyLogoutToken(r.Context(), string(body)) + if err != nil { + render.Status(r, http.StatusBadRequest) + return + } + + records, err := h.sidCache.Read(logoutToken.SessionId) + if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 { + render.Status(r, http.StatusOK) + return + } + + for _, record := range records { + err = h.accessTokenCache.Delete(string(record.Value)) + if errors.Is(err, microstore.ErrNotFound) { + render.Status(r, http.StatusOK) + return } - prov, _ := oidc.NewProvider( - context.WithValue(context.Background(), oauth2.HTTPClient, oidcHTTPClient), - p.config.OIDC.Issuer, - ) - logoutVerifier := ocisLogoutVerifier.NewLogoutVerifier(p.config.OIDC) - */ - w.Header().Set("Location", "https://todo") + } + render.Status(r, http.StatusOK) } diff --git a/services/proxy/pkg/middleware/oidc_auth.go b/services/proxy/pkg/middleware/oidc_auth.go index 95c7a1528..4341f537c 100644 --- a/services/proxy/pkg/middleware/oidc_auth.go +++ b/services/proxy/pkg/middleware/oidc_auth.go @@ -73,15 +73,7 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri } // TODO: use mClaims - aClaims, mClaims, err := m.oidcClient.VerifyAccessToken(req.Context(), token) - - vals := make([]string, len(mClaims)) - for k, v := range mClaims { - s, _ := base64.StdEncoding.DecodeString(v) - vals[k] = string(s) - } - fmt.Println(vals) - + aClaims, _, err := m.oidcClient.VerifyAccessToken(req.Context(), token) if err != nil { return nil, errors.Wrap(err, "failed to verify access token") }