aspnet / Security

[Archived] Middleware for security and authorization of web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
1.27k stars 600 forks source link

Discussion for Auth 2.0 #1338

Closed HaoK closed 6 years ago

HaoK commented 7 years ago

Related discussion thread of issues/questions about https://github.com/aspnet/Announcements/issues/262

joeaudette commented 7 years ago

Hi,

In the announcement for the breaking changes you said:

"In 1.0, it was possible to configure different Authentication middleware with branching, this is no longer possible with a single middleware and shared services across all branches. A workaround could be to use different schemes/options for each branch."

I am facing this problem and would appreciate some elaboration on specifically how I can use different schemes per branch. My solution can provision new tenants at runtime, so iterating through tenants in startup to add schemes is not a solution for me.

Wondering if you could provide some guidance on what I could override to dynamically resolve schemes and options per request. PostConfigureCookieAuthenticationOptions, looks like a potential candidate that might allow me to do that, but not sure if that allows me to adjust the options per request. IAuthenticationSchemeProvider sounds like another potential thing to implement.

Could you offer a suggestion of the right thing to implement/override to achieve a way to resolve auth scheme/options on a per request basis?

Thanks in advance for any advice!

HaoK commented 7 years ago

Alternatively you could also try replacing the IAuthenticationHandlerProvider to return the appropriate instance for your branch and use the same name in each branch. The default implementation of this is just a scoped dictionary cache: https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationHandlerProvider.cs

If you had a implementation that was branch aware, that should be a step in the right direction. You'll still have to figure out how to resolve the appropriate options, but you could do that via plugging in your own IOptionsMonitor that is branch aware.

joeaudette commented 7 years ago

Thank you! Really appreciate the prompt response! I will give IAuthenticationHandlerProvider a try.

Can you tell me what is the purpose of IOptionsMonitor? what is it used for? I'm familiar with IOptions of T but IOptionsMonitor is new to me.

HaoK commented 7 years ago

Its basically the same thing, it just adds the ability to name options IOptionsMonitor.Get(authenticationScheme) is what you will need to implement

joeaudette commented 7 years ago

Thanks! Wondering also about cookie name, in my current 1.1 solution I support tenants based on the first folder segment so for that I have been using different auth scheme names with the cookie named the same as the auth scheme so that is is possible to login to multiple tenants without cookies stepping on each other. ie since the host name is not different per tenant I have to account for the fact that the browser will share cookies for the host name and use different cookie names Can we still configure different cookie names or will they be named after the authscheme so it would be sufficient to use different auth scheme names per tenant to get different named cookies?

HaoK commented 7 years ago

You can configure them however you like the underlying options haven't changed that much

joeaudette commented 7 years ago

Awesome, thanks very much! Will let you know how it goes or if I run into any troubles, working on the netcore20 branch in my open source project here https://github.com/joeaudette/cloudscribe , just got it to build but mainly by commenting out the broken bits, now I will try to get it working again following your advice.

valeriob commented 7 years ago

I'm trying to bring our owin authentication code to aspnetcore 2.0, so much stuff have changed ! I hope i can bring there same functionality back, i'll give feedback on it. At first impact i see too much use of new() generic constraint, it's kinda a bad smell to be honest, i think the framework should not instantiate anything but let the user build his types providing the right hooks, like owin did, i see no reason why the framework should instantiate AuthenticationSchemeOptions and AuthenticationHandler.

Banashek commented 7 years ago

So I'm just trying to add a simple claim based on a boolean column in my ApplicationUser table in my database. This seems prohibitively difficult.

I set up an IClaimsTransformation implementation which would get the user from the database and add the claim depending on the column value. I'm guessing because it's registered as a transient service is the reason that it's being called every single request, so it seems to be the wrong approach in my case.

I might not be approaching the problem correctly, but I'm not sure what the correct way to solve this problem is. All I want to do is check if a ApplicationUser.IsEnabled property is true in my authorization.

I kinda just want a simple function isUserEnabled, but feel like I have to go through a SpringBeanComparerFactoryFacadeAdapterFactory to check this.

I'll put the problem clearly here: How do I authorize a user based on a boolean database column? Is there a way to do it without hitting the database on every request? (claims seem to be the right approach here) If claims are the right approach, how do I add a claim to a user? (All the auth docs talk about checking claims, but never adding them) If I need to update a claim on my user, how can I do this? (Say I want to enable a disabled user after they update their payment method)

Thanks for any advice available.

joeaudette commented 7 years ago

@HaoK thanks to your previous help I managed to get my multi-tenant cookie auth working again. Now I am moving on to working on getting my multi-tenant social auth working and could use more advice about what to implement in order to resolve correct MicrosoftAccountOptions per tenant. I am working against preview2 and I see there are additional changes after preview2 such as use of IOptionsSnapshot in preview2 vs IOptionsMonitor in the current code. Since I am targeting preview2 for the moment, I was able to handle the mutlit-tenant cookie auth by implementing a custom

IOptionsSnapshot<CookieAuthenticationOptions>.

Since that worked I figured a similar solution should work for Microsoft Account Authentication by implementing a custom

IOptionsSnapshot<MicrosoftAccountOptions>

But currently I'm getting an error:

    System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler`1.BuildChallengeUrl(AuthenticationProperties properties, String redirectUri) at Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler`1.<HandleChallengeAsync>d__11.MoveNext()

I had a similar null reference when I first implemented the cookie auth but was able to solve it by taking a dependency on

IPostConfigureOptions<CookieAuthenticationOptions>

and calling PostConfigure on my options which seemed to solve whatever was null on my cookie options.

But I don't see anything similar to that to call for my MicrosoftAccountOptions, there exists MicrosoftAccountConfigureOptions but it is internal so I cannot use it even if it would solve the problem.

Wonder if you could shed any light or advice on how best to override the options per request based on my resolved tenant. Mainly I need to adjust the clientid, clientsecret, and callbackpath per tenant.

joeaudette commented 7 years ago

@HaoK it looks like what is null on my MicorosoftAccountOptions is StateDataFormat

What would be the correct way to populate that?

HaoK commented 7 years ago

You need to something like https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs

joeaudette commented 7 years ago

@HaoK thank you! that got me past the null error, but now after redirecting back from microsoft I get

AggregateException: Unhandled remote failure. (OAuth token endpoint failure: Status: Unauthorized;Headers: Cache-Control: no-store, no-cache

any ideas about that?

joeaudette commented 7 years ago

wait it is saying invalid client, I thought I was using credentials that already worked, I will double check first that it works in my 1.1 branch as it did before

joeaudette commented 7 years ago

@HaoK thank you again, that got it working! Somehow my client secret got corrupted in the db but once I fixed that I was able to get it working! Really appreciate all your help!

valeriob commented 7 years ago

I'm starting to get the new model 😄 , it handles challenges in a more clean way, but:

All in all, good job !

valeriob commented 7 years ago

I have a question about IClaimsTransformation. In owin/katana, the OpenIdConnect middleware exposes a function OpenIdConnectAuthenticationNotifications.AuthorizationCodeReceived that allowed to craft a AuthenticationTicket with an ClaimsIdentity where i could query the database just once when the user authenticate and add custom claims. In the new model OnAuthorizationCodeReceived does not allow that and IClaimsTransformation.TransformAsync it's called for every request, so i would hit the database on every request. I would like to add custom claims before the SignIn (so they get persisted on the cookie for example), how can i do that ? Thanks

HaoK commented 7 years ago

Why can't you continue to use OnAuthorizationCodeRecieved?

valeriob commented 7 years ago

Hi @HaoK thanks it works ! i can replace the AuthorizationCodeReceivedContext.Principal.

Still i do not think it's a very good design, those functions are called Events, and they look like side effect free functions, instead you can change "something" on the input parameters and have some side effect. To see what you can affect you have to look at source code of the caller to understand what's going on and what you can do : image

I would call those Callbacks, and make the side effect explicit on the return type, so you are sure on what you can affect.

kevinchalet commented 7 years ago

Still i do not think it's a very good design, those functions are called Events, and they look like side effect free functions, instead you can change "something" on the input parameters and have some side effect.

That's exactly how good old events/event handlers work :sweat_smile:

E.g https://msdn.microsoft.com/en-us/library/system.componentmodel.canceleventargs(v=vs.110).aspx

mclasson commented 7 years ago

@Banashek You are correct. Just add a new Claim. Look at an example here https://marcusclasson.com/2017/08/03/adding-authentication-in-aspnetcore-2-0/ At the bottom I add a custom claim (like user id) Works in 2.0

plaisted commented 7 years ago

@joeaudette

I recently went through the same issues you were having where I needed different authentication for each branch. We have a multi-tenant setup where each branch/tenant would have their own certificates etc for JwtBearer.

I ended up with a different approach than was discussed here due to their comment "A workaround could be to use different schemes/options for each branch" and not seeing this thread until after completing the work. I thought I'd post here in case others are having issues as there isn't much guidance out there.

I set up all my different authentications for each branch with a specific scheme name:

foreach (var scheme in mySchemes)
{
    services.AddJwtBearerAuthentication(scheme.Name, options => { // scheme specific options here //});
}

I then created a custom IAuthenticationSchemeProvider that was identical to the default implementation except for injecting a IHttpContextAccessor to the constructor and changing all of the GetDefaults to be branch aware like:

//naive example:
public Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
{
    var schemeToUse = _context.HttpContext.Request.Path.Value.Split('/')[1];
    return GetSchemeAsync(schemeToUse);
}

This allowed me to keep everything else with the default implementation and have each individual scheme be unaware of any custom setup. I haven't fully vetted this as it's using Preview2 and is a POC but appears to be functioning properly.

joeaudette commented 7 years ago

Having some issues updating from preview2 to rtm. Where can I find the code for the default implementation of

IOptionsMonitor<CookieAuthenticationOptions>

?

joeaudette commented 7 years ago

nevermind, it is generic, I forgot https://github.com/aspnet/Options/blob/dev/src/Microsoft.Extensions.Options/OptionsMonitor.cs

sireza commented 7 years ago

Having issues porting our app to .NET Core 2.0

Background: This is an API app with Swagger access. The app uses Azure AD as provider Supported Use Cases:

The following works as above pre 2.0:

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
                AutomaticAuthenticate = true,
                AutomaticChallenge = true
            });

            app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
            {
                AutomaticChallenge = true,
                AutomaticAuthenticate = true,
                ClientId = Configuration["Authentication:ClientId"],
                Authority = Configuration["Authentication:Authority"],
                AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
                SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme
            });

            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                AutomaticChallenge = true,
                AutomaticAuthenticate = true,
                Audience = Configuration["Authentication:ClientId"],
                Authority = Configuration["Authentication:Authority"],
                AuthenticationScheme = JwtBearerDefaults.AuthenticationScheme
            });

            // Requires authentication to use swagger
            app.Use(async (ctx, next) =>
            {
                if (ctx.User?.Identity != null && ctx.User.Identity.IsAuthenticated)
                {
                    // If you're authenticated, proceed
                    await next();
                }
                else
                {
                    // If you're not authenticated, do not proceed
                    // and issue an authentication challenge
                    await ctx.Authentication.ChallengeAsync();
                }
            });

Tried the following after ported to 2.0

In ConfigureServices(...)

                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;

                })
                .AddCookie(options =>
                {
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;

                })
                .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
                {
                    options.Audience = Configuration["Authentication:ClientId"];
                    options.Authority = Configuration["Authentication:Authority"];

                })
                .AddOpenIdConnect(options =>
                {
                    options.ClientId = Configuration["Authentication:ClientId"];
                    options.Authority = Configuration["Authentication:Authority"];
                    options.ClientSecret = Configuration["Authentication:ClientSecret"];
                    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.ResponseType = OpenIdConnectResponseType.IdToken;
                });

In Configure(...)


            app.UseAuthentication();
            //Requires authentication to use swagger
            app.Use(async (ctx, next) =>
            {
                if (!env.IsDevelopment() || (ctx.User?.Identity != null && ctx.User.Identity.IsAuthenticated))
                {
                    // If you're authenticated, proceed
                    await next();
                }
                else
                {
                    // If you're not authenticated, do not proceed
                    // and issue an authentication challenge
                    await ctx.ChallengeAsync();
                }
            }); ```

What am I missing?
davidfowl commented 7 years ago

What behavior are you expecting? There's no more automatic* anything in 2.0, you can only have a single "default scheme".

sireza commented 7 years ago

@davidfowl hoping we could get browser user authenticated via Azure AD (OpenIDConnect) as well as other web application authenticated by providing a bearer token on the request header.

Currently, authentication via browser (when using Swagger) works, but other web app gets Http 302 (redirects) to Azure AD Login (i.e. not recognizing the provided bearer token on the header.

As previously mentioned, both of these scenario works pre-2.0

HaoK commented 7 years ago

You were probably relying on both cookies and bearer doing automatic authentication before. Only one can be the default now, the other you will need to explicitly ask for via Authenticate.

Of course you can always add your own custom middleware that does something like checking for the non default scheme (i.e. JwtBearer)'s principal via authenticate and jamming that into context.User yourself if there was no cookie set identity, to effectively get the old behavior.

sireza commented 7 years ago

@HaoK yup that works! thanks for the hint!

dcarr42 commented 7 years ago

Need this in samples imo

jeffputz commented 7 years ago

I'm having a hard time understanding how you can light up two identities via different schemes. In 1.1, I could do something like this:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationScheme = "Cookies",
    AutomaticAuthenticate = true
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationScheme = "pf",
    CookieName = "pf",
    AutomaticAuthenticate = true
});

Believe it or not, there were cases where I had two different kind of logins for different parts of the app, and this allowed me to hit the User.Identities item I wanted.

HaoK commented 7 years ago

If you want multiple cookie identities in context.User, you will need to have a custom middleware that merges them together.

You'd retreive the identity via context.AuthenticateAsync("pf"), and then construct a new ClaimsPrincipal with this, and merge the existing context.User.Identities in and set context.User to that merged principal.

Here's how we do the merge internally: https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.SecurityHelper.Sources/SecurityHelper.cs#L22

davidfowl commented 7 years ago

@jeffputz in your old code, how were you getting the "right" identity on a particular part of the site?

jeffputz commented 7 years ago

@davidfowl:

var identity = context.HttpContext.User.Identities.SingleOrDefault(x => x.AuthenticationType == "pf");

See line 49: https://github.com/POPWorldMedia/POPForums/blob/6f0f6b8f7698839e230da6ba59b3b2b6f1afc5d1/src/PopForums.Mvc/Areas/Forums/Authorization/PopForumsUserAttribute.cs

Several people using the app are doing so mostly because they can add it to their existing app and not collide with other stuff. Maybe that's a weird use case (I wouldn't build something that way), but I can see the reasoning.

I haven't tried migrating the social logins yet, but I assume those would have their own identity and set of claims too, right?

HaoK commented 7 years ago

var identity = context.HttpContext.User.Identities.SingleOrDefault(x => x.AuthenticationType == "pf");

Can this easily be replaced with context.HttpContext.AuthenticateAsync("pf") to get the ClaimsPrincipal? Then you won't need any of the weird merging logic.

davidfowl commented 7 years ago

My thoughts exactly

jeffputz commented 7 years ago

Ah, that works. It wasn't obvious, but it does work. Is this what the middleware does against the default scheme under the covers, putting the result on the user identity?

mattwoberts commented 7 years ago

Hiya

In my netcore 1.1 app, I configured both cookies and bearer auth. Neither are set to AutomaticChallenge or AutomaticAuthenticate. I then configure my controllers to determine which methods of auth are acceptable (some allow either cookie or bearer, others only allow cookie):

[Authorize(ActiveAuthenticationSchemes = "Cookies,Bearer")]

I'm not sure how to go about migrating this to Auth 2 - can you point me in the right direction?

davidfowl commented 7 years ago

I'm not sure how to go about migrating this to Auth 2 - can you point me in the right direction?

In Startup.ConfigureServices

services.AddAuthentication()
              .AddCookie(...)
              . AddJwtBearer(...)

In Startup.Configure

app.UseAuthentication();
js82 commented 7 years ago

I'm looking for some help regarding cookie authentication with WebApi.

My company is using .NET Core WebApi and Angular for a project. I simply want the cookie auth handler to return 401 and 403 as appropriate, not attempt to redirect. I was able to get this to work for us in 1.0 by copying the cookie auth handler and middleware to files on the solution and referencing that in Startup instead. All I had to do was comment out the parts that attempt a redirect.

I was hoping that the move to 2.0 and relevant security changes would make this easier, but I haven't had any luck. I've tried as many combinations that I can think of for AddAuthentication, AddCookie, AddCookieAuthentication (and their respective signatures) in Configure Services (along with UseAuthentication in Configure).

Visual Studio tells me that the CookieAuthenticationOptions properties LoginPath, LogoutPath, and AccessDeniedPath tell the handler it should change 401 and 403 into redirects. So I've tried setting those to null (and empty strings) in the configuration. No Luck.

Any guidance is appreciated.

plaisted commented 7 years ago

@js82

I believe you should be able to simply inject your custom CookieAuthenticationHandler after adding cookie authentication using the AuthenticationBuilder.AddCookie().

If you create MyCustomCookieHandler and inherit from the default CookieAuthenticationHandler and then override HandleChallengeAsync(AuthenticationProperties properties) to not redirect using something like:

        protected override Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            Response.StatusCode = 401;
            return Task.CompletedTask;
        }

you should get your desired behavior although I haven't specifically tested it. I think you'll need to inject using services.AddTransient<CookieAuthenticationHandler, MyCustomCookieHandler>(). Hopefully this gets you down the right path.

js82 commented 7 years ago

@plaisted

First of all, thank you! I implemented what you suggested and hitting a protected endpoint without a cookie now returns a 401 instead of attempting to redirect.

I was hoping I wouldn't need to add any custom handler code, but one file with one override is way better than what I had in 1.1.

I'll report back after additional testing against claims.

js82 commented 7 years ago

@plaisted

I also had to override HandleForbiddenAsync (I'm sure that's obvious to most), and I wonder if there are other parts I'll need to handle, but now it appears to be working as I had hoped!

Thanks again for this much more elegant solution that I no longer have to be embarrassed about!

plaisted commented 7 years ago

@js82 Great! Glad to help. I believe the Challenge (you aren't logged in and need to be) and Forbid (you are logged in and aren't allowed here) methods are all you'll need to change.

You may consider adding an appropriate WWW-Authenticate header to the Challenge response (see JwtBearerHandler for an example) to stay in spec with HTTP although if the site is only consumed by your services may not be necessary.

danjohnso commented 7 years ago

In 1.1 my apps used cookies + oidc for the main application, but I would map the path "/api" separately and do UseIdentityServerAuthentication for that path (basically JwtBearerTokens). How does this migrate to 2.0?

app.Map("/api", apiApp =>
{
    apiApp.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
    {
        Authority = identityUrl,
        ApiName = "api",
        EnableCaching = true,
        CacheDuration = TimeSpan.FromMinutes(10),
        AutomaticAuthenticate = true,
        AutomaticChallenge = true
    });
});
davidfowl commented 7 years ago

On your api controllers, you would just change the AuthorizeAttribute to specify the right scheme.

raymondle commented 7 years ago

Hi all, Right now i have trying with new Auth 2.0 flow, it very cool. But i have a question

When I'm using AuthenticationSchemes in BaseApiController to active my custom ApiAuthenticationHandler and i want to customs some methods have Roles permission to grant it. So i'm using [Authorize(Roles = "Administrator"].

But when i'm calling it, it redirect to LoginPage. It not return status code 403 as i want. And i'm trying re-custom AuthorizeAtrributes like [Authorize(Roles = "Administrator", AuthenticationSchemes = SchemeConstants.Api)] OMG it working

So how can i fix that? Thank you so much <3

    public class AccountController : BaseApiController
    {

        [Authorize(Roles = "Administrator", AuthenticationSchemes = SchemeConstants.Api)]
        public DataResponse Get()
        {

            return new DataResponse()
            {
                StatusCode = 200,
                Data = "Hello, here you are"
            };
        }
    }
    [Produces("application/json")]
    [Route("api/[controller]")]
    [Authorize(AuthenticationSchemes = SchemeConstants.Api)]
    public class BaseApiController : Controller
    {

    }

P/s: If i have any mistake about grammar, please forgive me :(

SirwanAfifi commented 7 years ago

I am not also able to port my ASP.NET Core 1.1 to 2.0, Here's the code I am using in my ASP.NET Core 1.1 but it seems that it doesn't not work anymore in version 2.0:

// ConfigureServices
services.AddIdentity<User, IdentityRole>()
            .AddEntityFrameworkStores<AppDbContext>()
            .AddDefaultTokenProviders();

services.Configure<IdentityOptions>(options =>
{
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequiredLength = 3;
});

services.ConfigureApplicationCookie(identityOptionsCookies => {
    var provider = services.BuildServiceProvider();
    identityOptionsCookies.AccessDeniedPath = new PathString("/Account/Login");
});

services.Configure<JwtIssuerOptions>(options =>
{
    options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
    options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
    options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});

// Configure method
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
var tokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
    ValidateAudience = true,
    ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = _signingKey,
    RequireExpirationTime = true,
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
};

app.UseWhen(
    c => c.Request.Path.StartsWithSegments("/api")
    ,
    a => a.UseJwtBearerAuthentication(new JwtBearerOptions
    {
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,
        TokenValidationParameters = tokenValidationParameters,
        AuthenticationScheme = JwtBearerDefaults.AuthenticationScheme
    })
);
app.UseWhen(
    c => c.Request.Path.StartsWithSegments("/api") == false
    ,
    a => a.UseAuthentication()
);

Any idea?

HaoK commented 7 years ago

@raymondle I'm not exactly clear what you are asking, can you describe what you are expecting and also include the startup code that is registering the auth for your app.

HaoK commented 7 years ago

@SirwanAfifi you won't be able to conditionally add the JwtBearer in 2.0 anymore, you can do something like this instead: