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 }