Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User.GetUserId() doesn't work while using token authentication #15

Closed
piotrek-k opened this issue Feb 23, 2016 · 8 comments
Closed

User.GetUserId() doesn't work while using token authentication #15

piotrek-k opened this issue Feb 23, 2016 · 8 comments

Comments

@piotrek-k
Copy link

User.GetUserId();
always returns null when I use controller with [Authorize("Bearer")] attribute. For not-api controllers (with [Authorize] everything seems to work OK).
I suspect that there is something to include in [ClaimsIdentity object here].
Could you help me with solving this?

@mrsheepuk
Copy link
Owner

Hi @mypcisbetter - I'm about 12,000 miles away from my computer at the moment, I'll have a look at this when I get back but it'll be at least two weeks before that.

@tessierp
Copy link

tessierp commented Mar 9, 2016

I'm having the same issue. Also, once I requested a token to the controller which is sent successfully to another WebAPI, Authorize wont work, probably because I need to set that information in the HttpContext that is where I am unsure what to do.

@John0x
Copy link

John0x commented Mar 9, 2016

This example doesn't really work with the Identity system, and therefore not User.GetUserId() won't work.

@vackup
Copy link

vackup commented Mar 9, 2016

Good morning guys, the problem here is that you are not setting up the identity claim correctly.

GetUserId is an extension method. If you check GetUserId source code you will see that the method basically looks for a claim called "ClaimTypes.NameIdentifier" (principal.FindFirstValue(ClaimTypes.NameIdentifier); )

So the solution to this is to add that claim in ClaimsIdentity in the TokenController, method GetToken after the comment
// Here, you should create or look up an identity for the user which is being authenticated.
// For now, just creating a simple generic identity."

eg:

ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "TokenAuth"),
                new[] {new Claim(ClaimTypes.NameIdentifier, userId)}); // userId is whatever you want to set here as a userId

Then in my CategoryController:

[HttpGet]
        [Authorize("Bearer")]
        public async Task<IEnumerable<Category>> GetCategory()
        {
            var userId = HttpContext.User.GetUserId();

            return await _categoryService.GetAllCategories(userId);
        }

Hope this can help. This is how I solved it.
Greetings from Argentina
Hernan
www.hernanzaldivar.com

@piotrek-k
Copy link
Author

Thanks @vackup. I think it solves the problem.

@piotrek-k
Copy link
Author

@vackup It seems like there is a problem somewhere...

It do work after running my web app, but after some time GetUserId() becomes NULL again.

Token that I use is exactly the same, everything on client side seems to be right. I'm making the same request each time. It works some time (usually 4-5 calls) and then it suddenly stop working (I mean: on server side GetUserId() returns null instead of id).

I suspected that it was something like token expiration, but it gives me access to data every time (so token works (and expirtation is set to half an hour so no, it isn't token expiration)), just Id is null... Is there something I don't know about this code? Why do you think it happens?

@vackup
Copy link

vackup commented Mar 15, 2016

@MyPCIsBetter mmmmm, that's weird because if token has expired, then you wouldn't be able to access your data.

Maybe you are refreshing the token somewhere and when creating the new token you are not setting up the userId correclty as you are doing the first time the token was created:

ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "TokenAuth"),
                new[] {new Claim(ClaimTypes.NameIdentifier, userId)}); // userId is whatever you want to set here as a userId

May be if you share your code we can try to figure out the issue.

By the way, asp.net 5 is still in RC, I mean it's not the final version so maybe there is a bug somewhere

@piotrek-k
Copy link
Author

@vackup
This is how I create token (TokenGenerator function returns this token):

    public class TokenAuthManager
    {
        public string Audience { get; set; }
        public string Issuer { get; set; }
        public SigningCredentials SigningCredentials { get; set; }

        public string TokenGenerator(string userId, string user, DateTime? expires)
        {
            var handler = new JwtSecurityTokenHandler();

            // Here, you should create or look up an identity for the user which is being authenticated.
            // For now, just creating a simple generic identity.
            ClaimsIdentity identity = new ClaimsIdentity(
                new GenericIdentity(user, "TokenAuth"),
                new[] {
                    new Claim("EntityID", "1", ClaimValueTypes.Integer),
                    new Claim(ClaimTypes.NameIdentifier, userId)
                });
            //identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId));

            var securityToken = handler.CreateToken(
                issuer: Issuer,
                audience: Audience,
                signingCredentials: SigningCredentials,
                subject: identity,
                expires: expires
                );
            return handler.WriteToken(securityToken);
        }
    }

This is when I create token (this is controler. I send token to the View using ViewData and there I assign it to javascript variable. I know it's weird what I've done here, I'll change it later):

[Authorize]
        public IActionResult Notepad()
        {
            //var a = db.Notes.Include(x => x.NoteTags).ToList();
            var a = User.GetUserId();
            ViewData["AuthToken"] = _tokenOptions.TokenGenerator(User.GetUserId(), User.Identity.Name, DateTime.UtcNow.AddMinutes(60));
            return View();
        }

Entire Startup.cs file (no big difference between what is in mrsheepuk's github repo, I just copy/pasted to simply make it work) :

public class Startup
    {
        const string TokenAudience = "ExampleAudience";
        const string TokenIssuer = "ExampleIssuer";
        private RsaSecurityKey key;
        private TokenAuthManager tokenOptions;

        public Startup(IHostingEnvironment env)
        {
            // Set up configuration sources.
            var builder = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

            if (env.IsDevelopment())
            {
                // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets();
            }

            builder.AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; set; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddEntityFramework()
                .AddSqlServer()
                .AddDbContext<AuthorizationDbContext>(options =>
                    options.UseSqlServer(Configuration["Data:DefaultConnection:AuthConnectionString"]))
                .AddDbContext<DataDbContext>(options =>
                    options.UseSqlServer(Configuration["Data:DefaultConnection:DataConnectionString"]));

            services.AddIdentity<ApplicationUser, IdentityRole>(o =>
            {
                // configure identity options
                o.Password.RequireDigit = false;
                o.Password.RequireLowercase = false;
                o.Password.RequireUppercase = false;
                o.Password.RequireNonLetterOrDigit = false; ;
                o.Password.RequiredLength = 6;
            })
                .AddEntityFrameworkStores<AuthorizationDbContext>()
                .AddDefaultTokenProviders();

            services.AddScoped<MySignInManager<ApplicationUser>, MySignInManager<ApplicationUser>>(); //rzekomo dzięki temu można zastąpić SignInManagera swoim własnym

            //Token-based authentication https://github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample
            // *** CHANGE THIS FOR PRODUCTION USE ***
            // Here, we're generating a random key to sign tokens - obviously this means
            // that each time the app is started the key will change, and multiple servers 
            // all have different keys. This should be changed to load a key from a file 
            // securely delivered to your application, controlled by configuration.
            //
            // See the RSAKeyUtils.GetKeyParameters method for an examle of loading from
            // a JSON file.
            RSAParameters keyParams = RSAKeyUtils.GetRandomKey();

            // Create the key, and a set of token options to record signing credentials 
            // using that key, along with the other parameters we will need in the 
            // token controlller.
            key = new RsaSecurityKey(keyParams);
            tokenOptions = new TokenAuthManager()
            {
                Audience = TokenAudience,
                Issuer = TokenIssuer,
                SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature)
            };

            // Save the token options into an instance so they're accessible to the 
            // controller.
            services.AddInstance<TokenAuthManager>(tokenOptions);

            // Enable the use of an [Authorize("Bearer")] attribute on methods and classes to protect.
            services.AddAuthorization(auth =>
            {
                auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
                    .RequireAuthenticatedUser().Build());
            });

            //Koniec Token-based authentication

            services.AddMvc();

            // Add application services.
            services.AddTransient<IEmailSender, AuthMessageSender>();
            services.AddTransient<ISmsSender, AuthMessageSender>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");

                // For more details on creating database during deployment see http://go.microsoft.com/fwlink/?LinkID=615859
                try
                {
                    using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
                        .CreateScope())
                    {
                        serviceScope.ServiceProvider.GetService<AuthorizationDbContext>()
                             .Database.Migrate();
                    }
                }
                catch { }
            }

            /*
            * Źródło: https://github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample
            */

            // Register a simple error handler to catch token expiries and change them to a 401, 
            // and return all other errors as a 500. This should almost certainly be improved for
            // a real application.
            app.UseExceptionHandler(appBuilder =>
            {
                appBuilder.Use(async (context, next) =>
                {
                    var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
                    // This should be much more intelligent - at the moment only expired 
                    // security tokens are caught - might be worth checking other possible 
                    // exceptions such as an invalid signature.
                    if (error != null && error.Error is SecurityTokenExpiredException)
                    {
                        context.Response.StatusCode = 401;
                        // What you choose to return here is up to you, in this case a simple 
                        // bit of JSON to say you're no longer authenticated.
                        context.Response.ContentType = "application/json";
                        await context.Response.WriteAsync(
                            JsonConvert.SerializeObject(
                                new { authenticated = false, tokenExpired = true }));
                    }
                    else if (error != null && error.Error != null)
                    {
                        context.Response.StatusCode = 500;
                        context.Response.ContentType = "application/json";
                        // TODO: Shouldn't pass the exception message straight out, change this.
                        await context.Response.WriteAsync(
                            JsonConvert.SerializeObject
                            (new { success = false, error = error.Error.Message }));
                    }
                    // We're not trying to handle anything else so just let the default 
                    // handler handle.
                    else await next();
                });
            });

            app.UseJwtBearerAuthentication(options =>
            {
                // Basic settings - signing key to validate with, audience and issuer.
                options.TokenValidationParameters.IssuerSigningKey = key;
                options.TokenValidationParameters.ValidAudience = tokenOptions.Audience;
                options.TokenValidationParameters.ValidIssuer = tokenOptions.Issuer;

                // When receiving a token, check that we've signed it.
                options.TokenValidationParameters.ValidateSignature = true;

                // When receiving a token, check that it is still valid.
                options.TokenValidationParameters.ValidateLifetime = true;

                // This defines the maximum allowable clock skew - i.e. provides a tolerance on the token expiry time 
                // when validating the lifetime. As we're creating the tokens locally and validating them on the same 
                // machines which should have synchronised time, this can be set to zero. Where external tokens are
                // used, some leeway here could be useful.
                options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(0);
            });
            //koniec kodu z tokenami

            app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear());

            app.UseStaticFiles();

            app.UseIdentity();

            // To configure external authentication please see http://go.microsoft.com/fwlink/?LinkID=532715

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Notepad}/{id?}");
            });

        }

        // Entry point for the application.
        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants