github.com/cs3org/reva/v2@v2.27.7/pkg/auth/manager/oidc/oidc.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  // Package oidc  verifies an OIDC token against the configured OIDC provider
    20  // and obtains the necessary claims to obtain user information.
    21  package oidc
    22  
    23  import (
    24  	"context"
    25  	"encoding/json"
    26  	"fmt"
    27  	"os"
    28  	"strings"
    29  	"time"
    30  
    31  	oidc "github.com/coreos/go-oidc/v3/oidc"
    32  	authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
    33  	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    34  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    35  	"github.com/cs3org/reva/v2/pkg/appctx"
    36  	"github.com/cs3org/reva/v2/pkg/auth"
    37  	"github.com/cs3org/reva/v2/pkg/auth/manager/registry"
    38  	"github.com/cs3org/reva/v2/pkg/auth/scope"
    39  	"github.com/cs3org/reva/v2/pkg/errtypes"
    40  	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
    41  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    42  	"github.com/cs3org/reva/v2/pkg/rhttp"
    43  	"github.com/cs3org/reva/v2/pkg/sharedconf"
    44  	"github.com/juliangruber/go-intersect"
    45  	"github.com/mitchellh/mapstructure"
    46  	"github.com/pkg/errors"
    47  	"golang.org/x/oauth2"
    48  )
    49  
    50  func init() {
    51  	registry.Register("oidc", New)
    52  }
    53  
    54  type mgr struct {
    55  	provider         *oidc.Provider // cached on first request
    56  	c                *config
    57  	oidcUsersMapping map[string]*oidcUserMapping
    58  }
    59  
    60  type config struct {
    61  	Insecure     bool   `mapstructure:"insecure" docs:"false;Whether to skip certificate checks when sending requests."`
    62  	Issuer       string `mapstructure:"issuer" docs:";The issuer of the OIDC token."`
    63  	IDClaim      string `mapstructure:"id_claim" docs:"sub;The claim containing the ID of the user."`
    64  	UIDClaim     string `mapstructure:"uid_claim" docs:";The claim containing the UID of the user."`
    65  	GIDClaim     string `mapstructure:"gid_claim" docs:";The claim containing the GID of the user."`
    66  	GatewaySvc   string `mapstructure:"gatewaysvc" docs:";The endpoint at which the GRPC gateway is exposed."`
    67  	UsersMapping string `mapstructure:"users_mapping" docs:"; The optional OIDC users mapping file path"`
    68  	GroupClaim   string `mapstructure:"group_claim" docs:"; The group claim to be looked up to map the user (default to 'groups')."`
    69  }
    70  
    71  type oidcUserMapping struct {
    72  	OIDCIssuer string `mapstructure:"oidc_issuer" json:"oidc_issuer"`
    73  	OIDCGroup  string `mapstructure:"oidc_group" json:"oidc_group"`
    74  	Username   string `mapstructure:"username" json:"username"`
    75  }
    76  
    77  func (c *config) init() {
    78  	if c.IDClaim == "" {
    79  		// sub is stable and defined as unique. the user manager needs to take care of the sub to user metadata lookup
    80  		c.IDClaim = "sub"
    81  	}
    82  	if c.GroupClaim == "" {
    83  		c.GroupClaim = "groups"
    84  	}
    85  	if c.UIDClaim == "" {
    86  		c.UIDClaim = "uid"
    87  	}
    88  	if c.GIDClaim == "" {
    89  		c.GIDClaim = "gid"
    90  	}
    91  
    92  	c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
    93  }
    94  
    95  func parseConfig(m map[string]interface{}) (*config, error) {
    96  	c := &config{}
    97  	if err := mapstructure.Decode(m, c); err != nil {
    98  		err = errors.Wrap(err, "error decoding conf")
    99  		return nil, err
   100  	}
   101  	return c, nil
   102  }
   103  
   104  // New returns an auth manager implementation that verifies the oidc token and obtains the user claims.
   105  func New(m map[string]interface{}) (auth.Manager, error) {
   106  	manager := &mgr{}
   107  	err := manager.Configure(m)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	return manager, nil
   112  }
   113  
   114  func (am *mgr) Configure(m map[string]interface{}) error {
   115  	c, err := parseConfig(m)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	c.init()
   120  	am.c = c
   121  
   122  	am.oidcUsersMapping = map[string]*oidcUserMapping{}
   123  	if c.UsersMapping == "" {
   124  		// no mapping defined, leave the map empty and move on
   125  		return nil
   126  	}
   127  
   128  	f, err := os.ReadFile(c.UsersMapping)
   129  	if err != nil {
   130  		return fmt.Errorf("oidc: error reading the users mapping file: +%v", err)
   131  	}
   132  	oidcUsers := []*oidcUserMapping{}
   133  	err = json.Unmarshal(f, &oidcUsers)
   134  	if err != nil {
   135  		return fmt.Errorf("oidc: error unmarshalling the users mapping file: +%v", err)
   136  	}
   137  	for _, u := range oidcUsers {
   138  		if _, found := am.oidcUsersMapping[u.OIDCGroup]; found {
   139  			return fmt.Errorf("oidc: mapping error, group \"%s\" is mapped to multiple users", u.OIDCGroup)
   140  		}
   141  		am.oidcUsersMapping[u.OIDCGroup] = u
   142  	}
   143  
   144  	return nil
   145  }
   146  
   147  // The clientID would be empty as we only need to validate the clientSecret variable
   148  // which contains the access token that we can use to contact the UserInfo endpoint
   149  // and get the user claims.
   150  func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
   151  	ctx = am.getOAuthCtx(ctx)
   152  	log := appctx.GetLogger(ctx)
   153  
   154  	oidcProvider, err := am.getOIDCProvider(ctx)
   155  	if err != nil {
   156  		return nil, nil, fmt.Errorf("oidc: error creating oidc provider: +%v", err)
   157  	}
   158  
   159  	oauth2Token := &oauth2.Token{
   160  		AccessToken: clientSecret,
   161  	}
   162  
   163  	// query the oidc provider for user info
   164  	userInfo, err := oidcProvider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
   165  	if err != nil {
   166  		return nil, nil, fmt.Errorf("oidc: error getting userinfo: +%v", err)
   167  	}
   168  
   169  	// claims contains the standard OIDC claims like iss, iat, aud, ... and any other non-standard one.
   170  	// TODO(labkode): make claims configuration dynamic from the config file so we can add arbitrary mappings from claims to user struct.
   171  	// For now, only the group claim is dynamic.
   172  	// TODO(labkode): may do like K8s does it: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go
   173  	var claims map[string]interface{}
   174  	if err := userInfo.Claims(&claims); err != nil {
   175  		return nil, nil, fmt.Errorf("oidc: error unmarshaling userinfo claims: %v", err)
   176  	}
   177  
   178  	log.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Msg("unmarshalled userinfo")
   179  
   180  	if claims["iss"] == nil { // This is not set in simplesamlphp
   181  		claims["iss"] = am.c.Issuer
   182  	}
   183  	if claims["email_verified"] == nil { // This is not set in simplesamlphp
   184  		claims["email_verified"] = false
   185  	}
   186  	if claims["preferred_username"] == nil {
   187  		claims["preferred_username"] = claims[am.c.IDClaim]
   188  	}
   189  	if claims["preferred_username"] == nil {
   190  		claims["preferred_username"] = claims["email"]
   191  	}
   192  	if claims["name"] == nil {
   193  		claims["name"] = claims[am.c.IDClaim]
   194  	}
   195  	if claims["name"] == nil {
   196  		return nil, nil, fmt.Errorf("no \"name\" attribute found in userinfo: maybe the client did not request the oidc \"profile\"-scope")
   197  	}
   198  	if claims["email"] == nil {
   199  		return nil, nil, fmt.Errorf("no \"email\" attribute found in userinfo: maybe the client did not request the oidc \"email\"-scope")
   200  	}
   201  
   202  	uid, _ := claims[am.c.UIDClaim].(float64)
   203  	claims[am.c.UIDClaim] = int64(uid) // in case the uid claim is missing and a mapping is to be performed, resolveUser() will populate it
   204  	// Note that if not, will silently carry a user with 0 uid, potentially problematic with storage providers
   205  	gid, _ := claims[am.c.GIDClaim].(float64)
   206  	claims[am.c.GIDClaim] = int64(gid)
   207  
   208  	err = am.resolveUser(ctx, claims)
   209  	if err != nil {
   210  		return nil, nil, errors.Wrapf(err, "oidc: error resolving username for external user '%v'", claims["email"])
   211  	}
   212  
   213  	userID := &user.UserId{
   214  		OpaqueId: claims[am.c.IDClaim].(string), // a stable non reassignable id
   215  		Idp:      claims["iss"].(string),        // in the scope of this issuer
   216  		Type:     getUserType(claims[am.c.IDClaim].(string)),
   217  	}
   218  
   219  	gwc, err := pool.GetGatewayServiceClient(am.c.GatewaySvc)
   220  	if err != nil {
   221  		return nil, nil, errors.Wrap(err, "oidc: error getting gateway grpc client")
   222  	}
   223  	getGroupsResp, err := gwc.GetUserGroups(ctx, &user.GetUserGroupsRequest{
   224  		UserId: userID,
   225  	})
   226  	if err != nil {
   227  		return nil, nil, errors.Wrapf(err, "oidc: error getting user groups for '%+v'", userID)
   228  	}
   229  	if getGroupsResp.Status.Code != rpc.Code_CODE_OK {
   230  		return nil, nil, status.NewErrorFromCode(getGroupsResp.Status.Code, "oidc")
   231  	}
   232  
   233  	u := &user.User{
   234  		Id:           userID,
   235  		Username:     claims["preferred_username"].(string),
   236  		Groups:       getGroupsResp.Groups,
   237  		Mail:         claims["email"].(string),
   238  		MailVerified: claims["email_verified"].(bool),
   239  		DisplayName:  claims["name"].(string),
   240  		UidNumber:    claims[am.c.UIDClaim].(int64),
   241  		GidNumber:    claims[am.c.GIDClaim].(int64),
   242  	}
   243  
   244  	var scopes map[string]*authpb.Scope
   245  	if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) {
   246  		scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
   247  		if err != nil {
   248  			return nil, nil, err
   249  		}
   250  	} else {
   251  		scopes, err = scope.AddOwnerScope(nil)
   252  		if err != nil {
   253  			return nil, nil, err
   254  		}
   255  	}
   256  
   257  	return u, scopes, nil
   258  }
   259  
   260  func (am *mgr) getOAuthCtx(ctx context.Context) context.Context {
   261  	// Sometimes for testing we need to skip the TLS check, that's why we need a
   262  	// custom HTTP client.
   263  	customHTTPClient := rhttp.GetHTTPClient(
   264  		rhttp.Context(ctx),
   265  		rhttp.Timeout(time.Second*10),
   266  		rhttp.Insecure(am.c.Insecure),
   267  		// Fixes connection fd leak which might be caused by provider-caching
   268  		rhttp.DisableKeepAlive(true),
   269  	)
   270  	ctx = context.WithValue(ctx, oauth2.HTTPClient, customHTTPClient)
   271  	return ctx
   272  }
   273  
   274  // getOIDCProvider returns a singleton OIDC provider
   275  func (am *mgr) getOIDCProvider(ctx context.Context) (*oidc.Provider, error) {
   276  	ctx = am.getOAuthCtx(ctx)
   277  	log := appctx.GetLogger(ctx)
   278  
   279  	if am.provider != nil {
   280  		return am.provider, nil
   281  	}
   282  
   283  	// Initialize a provider by specifying the issuer URL.
   284  	// Once initialized this is a singleton that is reused for further requests.
   285  	// The provider is responsible to verify the token sent by the client
   286  	// against the security keys oftentimes available in the .well-known endpoint.
   287  	provider, err := oidc.NewProvider(ctx, am.c.Issuer)
   288  
   289  	if err != nil {
   290  		log.Error().Err(err).Msg("oidc: error creating a new oidc provider")
   291  		return nil, fmt.Errorf("oidc: error creating a new oidc provider: %+v", err)
   292  	}
   293  
   294  	am.provider = provider
   295  	return am.provider, nil
   296  }
   297  
   298  func (am *mgr) resolveUser(ctx context.Context, claims map[string]interface{}) error {
   299  	if len(am.oidcUsersMapping) > 0 {
   300  		var username string
   301  
   302  		// map and discover the user's username when a mapping is defined
   303  		if claims[am.c.GroupClaim] == nil {
   304  			// we are required to perform a user mapping but the group claim is not available
   305  			return fmt.Errorf("no \"%s\" claim found in userinfo to map user", am.c.GroupClaim)
   306  		}
   307  		mappings := make([]string, 0, len(am.oidcUsersMapping))
   308  		for _, m := range am.oidcUsersMapping {
   309  			if m.OIDCIssuer == claims["iss"] {
   310  				mappings = append(mappings, m.OIDCGroup)
   311  			}
   312  		}
   313  
   314  		intersection := intersect.Simple(claims[am.c.GroupClaim], mappings)
   315  		if len(intersection) > 1 {
   316  			// multiple mappings are not implemented as we cannot decide which one to choose
   317  			return errtypes.PermissionDenied("more than one user mapping entry exists for the given group claims")
   318  		}
   319  		if len(intersection) == 0 {
   320  			return errtypes.PermissionDenied("no user mapping found for the given group claim(s)")
   321  		}
   322  		for _, m := range intersection {
   323  			username = am.oidcUsersMapping[m.(string)].Username
   324  		}
   325  
   326  		upsc, err := pool.GetUserProviderServiceClient(am.c.GatewaySvc)
   327  		if err != nil {
   328  			return errors.Wrap(err, "error getting user provider grpc client")
   329  		}
   330  		getUserByClaimResp, err := upsc.GetUserByClaim(ctx, &user.GetUserByClaimRequest{
   331  			Claim: "username",
   332  			Value: username,
   333  		})
   334  		if err != nil {
   335  			return errors.Wrapf(err, "error getting user by username '%v'", username)
   336  		}
   337  		if getUserByClaimResp.Status.Code != rpc.Code_CODE_OK {
   338  			return status.NewErrorFromCode(getUserByClaimResp.Status.Code, "oidc")
   339  		}
   340  
   341  		// take the properties of the mapped target user to override the claims
   342  		claims["preferred_username"] = username
   343  		claims[am.c.IDClaim] = getUserByClaimResp.GetUser().GetId().OpaqueId
   344  		claims["iss"] = getUserByClaimResp.GetUser().GetId().Idp
   345  		claims[am.c.UIDClaim] = getUserByClaimResp.GetUser().UidNumber
   346  		claims[am.c.GIDClaim] = getUserByClaimResp.GetUser().GidNumber
   347  		appctx.GetLogger(ctx).Debug().Str("username", username).Interface("claims", claims).Msg("resolveUser: claims overridden from mapped user")
   348  	}
   349  	return nil
   350  }
   351  
   352  func getUserType(upn string) user.UserType {
   353  	var t user.UserType
   354  	switch {
   355  	case strings.HasPrefix(upn, "guest"):
   356  		t = user.UserType_USER_TYPE_LIGHTWEIGHT
   357  	case strings.Contains(upn, "@"):
   358  		t = user.UserType_USER_TYPE_FEDERATED
   359  	default:
   360  		t = user.UserType_USER_TYPE_PRIMARY
   361  	}
   362  	return t
   363  }