use claims map instead of struct

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2021-07-21 15:37:12 +00:00
parent b148faada6
commit 1f3e963c29
18 changed files with 177 additions and 105 deletions

View File

@@ -33,6 +33,8 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl
logger: logger,
tokenManager: tokenManager,
userProvider: options.UserProvider,
userOIDCClaim: options.UserOIDCClaim,
userCS3Claim: options.UserCS3Claim,
autoProvisionAccounts: options.AutoprovisionAccounts,
}
}
@@ -44,8 +46,11 @@ type accountResolver struct {
tokenManager tokenPkg.Manager
userProvider backend.UserBackend
autoProvisionAccounts bool
userOIDCClaim string
userCS3Claim string
}
// TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39
func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
claims := oidc.FromContext(req.Context())
u, ok := revauser.ContextGetUser(req.Context())
@@ -56,30 +61,31 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
if u == nil && claims != nil {
var claim, value string
switch {
case claims.PreferredUsername != "":
claim, value = "username", claims.PreferredUsername
case claims.Email != "":
claim, value = "mail", claims.Email
case claims.OcisID != "":
//claim, value = "id", claims.OcisID
default:
// TODO allow lookup by custom claim, eg an id ... or sub
m.logger.Error().Msg("Could not lookup account, no mail or preferred_username claim set")
w.WriteHeader(http.StatusInternalServerError)
}
var err error
u, err = m.userProvider.GetUserByClaims(req.Context(), claim, value, true)
var value string
var ok bool
if value, ok = claims[m.userOIDCClaim].(string); !ok || value == "" {
m.logger.Error().Str("claim", m.userOIDCClaim).Interface("claims", claims).Msg("claim not set or empty")
w.WriteHeader(http.StatusInternalServerError) // admin needs to make the idp send the right claim
return
}
if m.autoProvisionAccounts && err == backend.ErrAccountNotFound {
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("User by claim not found... autoprovisioning.")
u, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value, true)
if err == backend.ErrAccountNotFound {
m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Msg("User by claim not found")
if !m.autoProvisionAccounts {
m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning disabled")
w.WriteHeader(http.StatusUnauthorized)
return
}
m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning user")
u, err = m.userProvider.CreateUserFromClaims(req.Context(), claims)
}
if err == backend.ErrAccountNotFound || err == backend.ErrAccountDisabled {
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("Unautorized")
if err == backend.ErrAccountDisabled {
m.logger.Debug().Interface("claims", claims).Msg("Disabled")
w.WriteHeader(http.StatusUnauthorized)
return
}
@@ -90,17 +96,17 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msgf("associated claims with uuid")
m.logger.Debug().Interface("claims", claims).Interface("user", u).Msg("associated claims with user")
}
s, err := scope.AddOwnerScope(nil)
if err != nil {
m.logger.Error().Err(err).Msgf("could not get owner scope")
m.logger.Error().Err(err).Msg("could not get owner scope")
return
}
token, err := m.tokenManager.MintToken(req.Context(), u, s)
if err != nil {
m.logger.Error().Err(err).Msgf("could not mint token")
m.logger.Error().Err(err).Msg("could not mint token")
w.WriteHeader(http.StatusInternalServerError)
return
}

View File

@@ -2,7 +2,11 @@ package middleware
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"net/http"
"net/http/httptest"
"testing"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/pkg/token"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/oidc"
@@ -10,20 +14,17 @@ import (
"github.com/owncloud/ocis/proxy/pkg/user/backend"
"github.com/owncloud/ocis/proxy/pkg/user/backend/test"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestTokenIsAddedWithMailClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil)
}, nil, oidc.Email, "mail")
req, rw := mockRequest(&oidc.StandardClaims{
Iss: "https://idx.example.com",
Email: "foo@example.com",
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.Email: "foo@example.com",
})
sut.ServeHTTP(rw, req)
@@ -37,11 +38,11 @@ func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
sut := newMockAccountResolver(&userv1beta1.User{
Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"},
Mail: "foo@example.com",
}, nil)
}, nil, oidc.PreferredUsername, "username")
req, rw := mockRequest(&oidc.StandardClaims{
Iss: "https://idx.example.com",
PreferredUsername: "foo",
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
})
sut.ServeHTTP(rw, req)
@@ -53,7 +54,7 @@ func TestTokenIsAddedWithUsernameClaim(t *testing.T) {
}
func TestNSkipOnNoClaims(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled)
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.Email, "mail")
req, rw := mockRequest(nil)
sut.ServeHTTP(rw, req)
@@ -64,10 +65,10 @@ func TestNSkipOnNoClaims(t *testing.T) {
}
func TestUnauthorizedOnUserNotFound(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound)
req, rw := mockRequest(&oidc.StandardClaims{
Iss: "https://idx.example.com",
PreferredUsername: "foo",
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.PreferredUsername, "username")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
})
sut.ServeHTTP(rw, req)
@@ -78,10 +79,10 @@ func TestUnauthorizedOnUserNotFound(t *testing.T) {
}
func TestUnauthorizedOnUserDisabled(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled)
req, rw := mockRequest(&oidc.StandardClaims{
Iss: "https://idx.example.com",
PreferredUsername: "foo",
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled, oidc.PreferredUsername, "username")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
oidc.PreferredUsername: "foo",
})
sut.ServeHTTP(rw, req)
@@ -92,9 +93,9 @@ func TestUnauthorizedOnUserDisabled(t *testing.T) {
}
func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
sut := newMockAccountResolver(nil, backend.ErrAccountDisabled)
req, rw := mockRequest(&oidc.StandardClaims{
Iss: "https://idx.example.com",
sut := newMockAccountResolver(nil, backend.ErrAccountNotFound, oidc.Email, "mail")
req, rw := mockRequest(map[string]interface{}{
oidc.Iss: "https://idx.example.com",
})
sut.ServeHTTP(rw, req)
@@ -104,7 +105,7 @@ func TestInternalServerErrorOnMissingMailAndUsername(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, rw.Code)
}
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error) http.Handler {
func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr error, oidcclaim, cs3claim string) http.Handler {
mock := &test.UserBackendMock{
GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, error) {
return userBackendResult, userBackendErr
@@ -115,11 +116,13 @@ func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr
Logger(log.NewLogger()),
UserProvider(mock),
TokenManagerConfig(config.TokenManager{JWTSecret: "secret"}),
UserOIDCClaim(oidcclaim),
UserCS3Claim(cs3claim),
AutoprovisionAccounts(false),
)(mockHandler{})
}
func mockRequest(claims *oidc.StandardClaims) (*http.Request, *httptest.ResponseRecorder) {
func mockRequest(claims map[string]interface{}) (*http.Request, *httptest.ResponseRecorder) {
if claims == nil {
return httptest.NewRequest("GET", "http://example.com/foo", nil), httptest.NewRecorder()
}

View File

@@ -82,11 +82,12 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
return
}
claims := &oidc.StandardClaims{
OcisID: user.Id.OpaqueId,
Iss: user.Id.Idp,
PreferredUsername: user.Username,
Email: user.Mail,
// fake oidc claims
claims := map[string]interface{}{
oidc.OwncloudUUID: user.Id.OpaqueId,
oidc.Iss: user.Id.Idp,
oidc.PreferredUsername: user.Username,
oidc.Email: user.Mail,
}
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))

View File

@@ -59,11 +59,11 @@ func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
}
// inject claims to the request context for the account_uuid middleware.
req = req.WithContext(oidc.NewContext(req.Context(), &claims))
req = req.WithContext(oidc.NewContext(req.Context(), claims))
// store claims in context
// uses the original context, not the one with probably reduced security
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), &claims)))
next.ServeHTTP(w, req.WithContext(oidc.NewContext(req.Context(), claims)))
})
}
}
@@ -78,7 +78,7 @@ type oidcAuth struct {
tokenCacheTTL time.Duration
}
func (m oidcAuth) getClaims(token string, req *http.Request) (claims oidc.StandardClaims, status int) {
func (m oidcAuth) getClaims(token string, req *http.Request) (claims map[string]interface{}, status int) {
hit := m.tokenCache.Load(token)
if hit == nil {
// TODO cache userinfo for access token if we can determine the expiry (which works in case it is a jwt based access token)
@@ -96,16 +96,12 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims oidc.Standa
return
}
// TODO allow extracting arbitrary claims ... or require idp to send a specific claim
if err := userInfo.Claims(&claims); err != nil {
m.logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims")
status = http.StatusInternalServerError
return
}
//TODO: This should be read from the token instead of config
claims.Iss = m.oidcIss
expiration := m.extractExpiration(token)
m.tokenCache.Store(token, claims, expiration)
@@ -114,7 +110,7 @@ func (m oidcAuth) getClaims(token string, req *http.Request) (claims oidc.Standa
}
var ok bool
if claims, ok = hit.V.(oidc.StandardClaims); !ok {
if claims, ok = hit.V.(map[string]interface{}); !ok {
status = http.StatusInternalServerError
return
}

View File

@@ -41,6 +41,10 @@ type Options struct {
Store storepb.StoreService
// PreSignedURLConfig to configure the middleware
PreSignedURLConfig config.PreSignedURL
// UserOIDCClaim to read from the oidc claims
UserOIDCClaim string
// UserCS3Claim to use when looking up a user in the CS3 API
UserCS3Claim string
// AutoprovisionAccounts when an accountResolver does not exist.
AutoprovisionAccounts bool
// EnableBasicAuth to allow basic auth
@@ -141,6 +145,20 @@ func PreSignedURLConfig(cfg config.PreSignedURL) Option {
}
}
// UserOIDCClaim provides a function to set the UserClaim config
func UserOIDCClaim(val string) Option {
return func(o *Options) {
o.UserOIDCClaim = val
}
}
// UserCS3Claim provides a function to set the UserClaimType config
func UserCS3Claim(val string) Option {
return func(o *Options) {
o.UserCS3Claim = val
}
}
// AutoprovisionAccounts provides a function to set the AutoprovisionAccounts config
func AutoprovisionAccounts(val bool) Option {
return func(o *Options) {