mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-02 02:11:18 -06:00
committed by
Christian Richter
parent
b608d0b0f9
commit
bc15b8a396
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user