package auth import ( "context" stderrors "errors" "fmt" "net/http" "strings" "time" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" "github.com/hashicorp/boundary/internal/gen/controller/tokens" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/sdk/recovery" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/mr-tron/base58" "google.golang.org/protobuf/proto" ) type TokenFormat int const ( // We weren't given one or couldn't parse it AuthTokenTypeUnknown TokenFormat = iota // Came in via the Authentication: Bearer header AuthTokenTypeBearer // Came in via split cookies AuthTokenTypeSplitCookie // It's of recovery type AuthTokenTypeRecoveryKms ) const ( AnonymousUserId = "u_anon" ) type key int var verifierKey key // RequestInfo contains request parameters necessary for checking authn/authz type RequestInfo struct { Path string Method string PublicId string EncryptedToken string Token string TokenFormat TokenFormat // The following are useful for tests scopeIdOverride string userIdOverride string DisableAuthzFailures bool DisableAuthEntirely bool } type VerifyResults struct { UserId string AuthTokenId string Error error Scope *scopes.ScopeInfo // AuthenticatedFinished means that the request has passed through the // authentication system successfully. This does _not_ indicate whether a // token was provided on the request. Requests for `u_anon` will still have // this set true! This is because if a request has a token that is invalid, // we fall back to `u_anon` because the request may still be allowed for any // anonymous user; it simply fails to validate for and look up grants for an // actual known user. // // A good example is when running dev mode twice. The first time you can // authenticate and get a token which is saved by the token helper. The // second time, you run a command and it reads the token from the helper. // That token is no longer valid, but if the action is granted to `u_anon` // the action should still succeed. What happens internally is that the // token fails to look up a non-anonymous user, so we fallback to the // anonymous user, which is the default. // // If you want to know if the request had a valid token provided, use a // switch on UserId. Anything that isn't `u_anon` will have to have had a // valid token provided. And a valid token will never fall back to `u_anon`. AuthenticationFinished bool // RoundTripValue can be set to allow the function performing authentication // (often accompanied by lookup(s)) to return a result of that lookup to the // calling function. It is opaque to this package. RoundTripValue interface{} // Used for additional verification v *verifier } type verifier struct { iamRepoFn common.IamRepoFactory authTokenRepoFn common.AuthTokenRepoFactory serversRepoFn common.ServersRepoFactory kms *kms.Kms requestInfo RequestInfo res *perms.Resource act action.Type ctx context.Context acl perms.ACL } // NewVerifierContext creates a context that carries a verifier object from the // HTTP handlers to the gRPC service handlers. It should only be created in the // HTTP handler and should exist for every request that reaches the service // handlers. func NewVerifierContext(ctx context.Context, iamRepoFn common.IamRepoFactory, authTokenRepoFn common.AuthTokenRepoFactory, serversRepoFn common.ServersRepoFactory, kms *kms.Kms, requestInfo RequestInfo) context.Context { return context.WithValue(ctx, verifierKey, &verifier{ iamRepoFn: iamRepoFn, authTokenRepoFn: authTokenRepoFn, serversRepoFn: serversRepoFn, kms: kms, requestInfo: requestInfo, }) } // Verify takes in a context that has expected parameters as values and runs an // authn/authz check. It returns a user ID, the scope ID for the request (which // may come from the URL and may come from the token) and whether or not to // proceed, e.g. whether the authn/authz check resulted in failure. If an error // occurs it's logged to the system log. func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { const op = "auth.Verify" ret.Error = handlers.ForbiddenError() v, ok := ctx.Value(verifierKey).(*verifier) if !ok { // We don't have a logger yet and this should never happen in any // context we won't catch in tests panic("no verifier information found in context") } ret.v = v v.ctx = ctx opts := getOpts(opt...) ret.Scope = new(scopes.ScopeInfo) // Note: we don't call RequestContextFromCtx here because that performs a // SelfOrDefault. That's useful in the handlers, but here we don't want to // do anything if it's nil. That's mainly useful in tests where // DisableAuthEntirely is set. Later, if we don't have the value set at all, // we have some safeguards to ensure we fail safe (e.g. no output fields at // all). So it provides a good indication as well whether we have set this // where and when needed. var reqInfo *requests.RequestContext reqInfoRaw := ctx.Value(requests.ContextRequestInformationKey) if reqInfoRaw != nil { reqInfo = reqInfoRaw.(*requests.RequestContext) } // In tests we often simply disable auth so we can test the service handlers // without fuss if v.requestInfo.DisableAuthEntirely { const op = "auth.(disabled).lookupScope" ret.Scope.Id = v.requestInfo.scopeIdOverride if ret.Scope.Id == "" { ret.Scope.Id = opts.withScopeId } // Look up scope details to return. We can skip a lookup when using the // global scope switch ret.Scope.Id { case "global": ret.Scope = &scopes.ScopeInfo{ Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope", ParentScopeId: "", } default: iamRepo, err := v.iamRepoFn() if err != nil { ret.Error = errors.Wrap(ctx, err, op, errors.WithMsg("failed to get iam repo")) return } scp, err := iamRepo.LookupScope(v.ctx, ret.Scope.Id) if err != nil { ret.Error = errors.Wrap(ctx, err, op) return } if scp == nil { ret.Error = errors.New(ctx, errors.InvalidParameter, op, fmt.Sprint("non-existent scope $q", ret.Scope.Id)) return } ret.Scope = &scopes.ScopeInfo{ Id: scp.GetPublicId(), Type: scp.GetType(), Name: scp.GetName(), Description: scp.GetDescription(), ParentScopeId: scp.GetParentId(), } } ret.UserId = v.requestInfo.userIdOverride if reqInfo != nil { reqInfo.UserId = ret.UserId } ret.Error = nil return } v.act = opts.withAction v.res = &perms.Resource{ ScopeId: opts.withScopeId, Id: opts.withId, Pin: opts.withPin, Type: opts.withType, } // Global scope has no parent ID; account for this if opts.withId == scope.Global.String() && opts.withType == resource.Scope { v.res.ScopeId = scope.Global.String() } if v.requestInfo.EncryptedToken != "" { v.decryptToken(ctx) } var authResults perms.ACLResults var err error authResults, ret.UserId, ret.Scope, v.acl, err = v.performAuthCheck(ctx) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error performing authn/authz check")) return } ret.AuthTokenId = v.requestInfo.PublicId ret.AuthenticationFinished = authResults.AuthenticationFinished if !authResults.Authorized { if v.requestInfo.DisableAuthzFailures { ret.Error = nil // TODO: Decide whether to remove this err := event.WriteObservation(ctx, op, event.WithHeader( "auth-results", struct { Msg string `json:"msg"` Resource *perms.Resource `json:"resource"` UserId string `json:"user_id"` Action string `json:"action"` }{ Msg: "failed authz info for request", Resource: v.res, UserId: ret.UserId, Action: v.act.String(), })) if err != nil { event.WriteError(ctx, op, err) } } else { // If the anon user was used (either no token, or invalid (perhaps // expired) token), return a 401. That way if it's an authn'd user // that is not authz'd we'll return 403 to be explicit. if ret.UserId == AnonymousUserId { ret.Error = handlers.UnauthenticatedError() } return } } if reqInfo != nil { reqInfo.UserId = ret.UserId reqInfo.OutputFields = authResults.OutputFields } ret.Error = nil return } func (v *verifier) decryptToken(ctx context.Context) { const op = "auth.(verifier).decryptToken" switch v.requestInfo.TokenFormat { case AuthTokenTypeUnknown: // Nothing to decrypt return case AuthTokenTypeBearer, AuthTokenTypeSplitCookie: if v.kms == nil { event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } tokenRepo, err := v.authTokenRepoFn() if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to get authtoken repo")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } at, err := tokenRepo.LookupAuthToken(v.ctx, v.requestInfo.PublicId) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to look up auth token by public ID")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } if at == nil { event.WriteError(ctx, op, stderrors.New("nil result from looking up auth token by public ID")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } tokenWrapper, err := v.kms.GetWrapper(v.ctx, at.GetScopeId(), kms.KeyPurposeTokens) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("unable to get wrapper for tokens; continuing as anonymous user")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } version := v.requestInfo.EncryptedToken[0:len(globals.ServiceTokenV1)] switch version { case globals.ServiceTokenV1: default: event.WriteError(ctx, op, stderrors.New("unknown token encryption version; continuing as anonymous user"), event.WithInfo("version", version)) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } marshaledToken, err := base58.FastBase58Decoding(v.requestInfo.EncryptedToken[len(globals.ServiceTokenV1):]) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling base58 token; continuing as anonymous user")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } blobInfo := new(wrapping.EncryptedBlobInfo) if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error decoding encrypted token; continuing as anonymous user")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } s1Bytes, err := tokenWrapper.Decrypt(v.ctx, blobInfo, []byte(v.requestInfo.PublicId)) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error decrypting encrypted token; continuing as anonymous user")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } var s1Info tokens.S1TokenInfo if err := proto.Unmarshal(s1Bytes, &s1Info); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling token info; continuing as anonymous user")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } if v.requestInfo.TokenFormat == AuthTokenTypeUnknown || s1Info.Token == "" || v.requestInfo.PublicId == "" { event.WriteError(ctx, op, stderrors.New("after parsing, could not find valid token; continuing as anonymous user")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } v.requestInfo.Token = s1Info.Token return case AuthTokenTypeRecoveryKms: if v.kms == nil { event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } wrapper := v.kms.GetExternalWrappers().Recovery() if wrapper == nil { event.WriteError(ctx, op, stderrors.New("no recovery KMS is available")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.EncryptedToken) if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error parsing and validating recovery token")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } // If we add the validity period to the creation time (which we've // verified is before the current time, with a minute of fudging), and // it's before now, it's expired and might be a replay. if info.CreationTime.Add(globals.RecoveryTokenValidityPeriod).Before(time.Now()) { event.WriteError(ctx, op, stderrors.New("recovery token has expired (possible replay attack)")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } repo, err := v.serversRepoFn() if err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error fetching servers repo")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } if err := repo.AddRecoveryNonce(v.ctx, info.Nonce); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error adding nonce to database (possible replay attack)")) v.requestInfo.TokenFormat = AuthTokenTypeUnknown return } event.WriteError(ctx, op, stderrors.New("recovery KMS was used to authorize a call"), event.WithInfo("url", v.requestInfo.Path, "method", v.requestInfo.Method)) } } func (v verifier) performAuthCheck(ctx context.Context) (aclResults perms.ACLResults, userId string, scopeInfo *scopes.ScopeInfo, retAcl perms.ACL, retErr error) { const op = "auth.(verifier).performAuthCheck" // Ensure we return an error by default if we forget to set this somewhere retErr = errors.NewDeprecated(errors.Unknown, op, "default auth error", errors.WithoutEvent()) // Make the linter happy _ = retErr scopeInfo = new(scopes.ScopeInfo) userId = AnonymousUserId var accountId string // Validate the token and fetch the corresponding user ID switch v.requestInfo.TokenFormat { case AuthTokenTypeUnknown: // Nothing; remain as the anonymous user case AuthTokenTypeRecoveryKms: // We validated the encrypted token in decryptToken and handled the // nonces there, so just set the user userId = "u_recovery" case AuthTokenTypeBearer, AuthTokenTypeSplitCookie: if v.requestInfo.Token == "" { // This will end up staying as the anonymous user break } tokenRepo, err := v.authTokenRepoFn() if err != nil { retErr = errors.WrapDeprecated(err, op) return } at, err := tokenRepo.ValidateToken(v.ctx, v.requestInfo.PublicId, v.requestInfo.Token) if err != nil { // Continue as the anonymous user as maybe this token is expired but // we can still perform the action event.WriteError(ctx, op, err, event.WithInfoMsg("error validating token; continuing as anonymous user")) break } if at != nil { accountId = at.GetAuthAccountId() userId = at.GetIamUserId() if userId == "" { event.WriteError(ctx, op, stderrors.New("perform auth check: valid token did not map to a user, likely because no account is associated with the user any longer; continuing as u_anon"), event.WithInfo("token_id", at.GetPublicId())) userId = AnonymousUserId accountId = "" } } } iamRepo, err := v.iamRepoFn() if err != nil { retErr = errors.WrapDeprecated(err, op, errors.WithMsg("failed to get iam repo")) return } // Look up scope details to return. We can skip a lookup when using the // global scope switch v.res.ScopeId { case "global": scopeInfo = &scopes.ScopeInfo{ Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope", ParentScopeId: "", } default: scp, err := iamRepo.LookupScope(v.ctx, v.res.ScopeId) if err != nil { retErr = errors.WrapDeprecated(err, op) return } if scp == nil { retErr = errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprint("non-existent scope $q", v.res.ScopeId)) return } scopeInfo = &scopes.ScopeInfo{ Id: scp.GetPublicId(), Type: scp.GetType(), Name: scp.GetName(), Description: scp.GetDescription(), ParentScopeId: scp.GetParentId(), } } // At this point we don't need to look up grants since it's automatically allowed if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms { aclResults.AuthenticationFinished = true aclResults.Authorized = true retErr = nil return } var parsedGrants []perms.Grant var grantTuples []perms.GrantTuple // Fetch and parse grants for this user ID (which may include grants for // u_anon and u_auth) grantTuples, err = iamRepo.GrantsForUser(v.ctx, userId) if err != nil { retErr = errors.WrapDeprecated(err, op) return } parsedGrants = make([]perms.Grant, 0, len(grantTuples)) // Note: Below, we always skip validation so that we don't error on formats // that we've since restricted, e.g. "id=foo;actions=create,read". These // will simply not have an effect. for _, pair := range grantTuples { parsed, err := perms.Parse( pair.ScopeId, pair.Grant, perms.WithUserId(userId), perms.WithAccountId(accountId), perms.WithSkipFinalValidation(true)) if err != nil { retErr = errors.WrapDeprecated(err, op, errors.WithMsg(fmt.Sprintf("failed to parse grant %#v", pair.Grant))) return } parsedGrants = append(parsedGrants, parsed) } retAcl = perms.NewACL(parsedGrants...) aclResults = retAcl.Allowed(*v.res, v.act) // We don't set authenticated above because setting this but not authorized // is used for further permissions checks, such as during recursive listing. // So we want to make sure any code relying on that has the full set of // grants successfully loaded. aclResults.AuthenticationFinished = true retErr = nil return } // FetchActionSetForId returns the allowed actions for a given ID using the // current set of ACLs and all other parameters the same (user, etc.) func (r *VerifyResults) FetchActionSetForId(ctx context.Context, id string, availableActions action.ActionSet, opt ...Option) action.ActionSet { return r.fetchActions(id, resource.Unknown, availableActions, opt...) } // FetchActionSetForType returns the allowed actions for a given collection type // using the current set of ACLs and all other parameters the same (user, etc.) func (r *VerifyResults) FetchActionSetForType(ctx context.Context, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet { return r.fetchActions("", typ, availableActions, opt...) } func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet { switch { case r.v.requestInfo.DisableAuthEntirely, r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms: // TODO: See if we can be better about what we return with the anonymous // user and recovery KMS, perhaps given exclusionary options for each return availableActions } opts := getOpts(opt...) res := opts.withResource // If not passed in, use what's already been populated through verification if res == nil { res = r.v.res } // If this is being called directly we may not have a resource yet if res == nil { res = new(perms.Resource) } if id != "" { res.Id = id } if typ != resource.Unknown { res.Type = typ } ret := make(action.ActionSet, 0, len(availableActions)) for _, act := range availableActions { if r.v.acl.Allowed(*res, act).Authorized { ret = append(ret, act) } } if len(ret) == 0 { return nil } return ret } func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) perms.OutputFieldsMap { switch { case r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms: return perms.OutputFieldsMap{"*": true} case r.v.requestInfo.DisableAuthEntirely: return nil } return r.v.acl.Allowed(res, act).OutputFields } // GetTokenFromRequest pulls the token from either the Authorization header or // split cookies and parses it. If it cannot be parsed successfully, the issue // is logged and we return blank, so logic will continue as the anonymous user. // The public ID and _encrypted_ token are returned along with the token format. func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Request) (string, string, TokenFormat) { const op = "auth.GetTokenFromRequest" // First, get the token, either from the authorization header or from split // cookies var receivedTokenType TokenFormat var fullToken string if authHeader := req.Header.Get("Authorization"); authHeader != "" { headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2) if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") { receivedTokenType = AuthTokenTypeBearer fullToken = strings.TrimSpace(headerSplit[1]) } } if receivedTokenType != AuthTokenTypeBearer { var httpCookiePayload string var jsCookiePayload string if hc, err := req.Cookie(handlers.HttpOnlyCookieName); err == nil { httpCookiePayload = hc.Value } if jc, err := req.Cookie(handlers.JsVisibleCookieName); err == nil { jsCookiePayload = jc.Value } if httpCookiePayload != "" && jsCookiePayload != "" { receivedTokenType = AuthTokenTypeSplitCookie fullToken = jsCookiePayload + httpCookiePayload } } if receivedTokenType == AuthTokenTypeUnknown || fullToken == "" { // We didn't find auth info or a client screwed up and put in a blank // header instead of nothing at all, so return blank which will indicate // the anonymouse user return "", "", AuthTokenTypeUnknown } if strings.HasPrefix(fullToken, "r_") { return "", fullToken, AuthTokenTypeRecoveryKms } splitFullToken := strings.Split(fullToken, "_") if len(splitFullToken) != 3 { event.WriteError(ctx, op, stderrors.New("unexpected number of segments in token; continuing as anonymous user"), event.WithInfo("expected", 3, "found", len(splitFullToken))) return "", "", AuthTokenTypeUnknown } publicId := strings.Join(splitFullToken[0:2], "_") encryptedToken := splitFullToken[2] return publicId, encryptedToken, receivedTokenType }