dev

Single User Login in .NET WebApi Core (via JWT token)

Outline

We were requested to incorporate functionality that ensure single user login (access). It means that if another client logs into the application, the first client (already logged in) will be logged out during his next request.

Because in the app we are using JWT token, as described in the previous post, we incorporated this functionality into this approach.

Workflow

The process workflow is as follows:

  1. User1 logs into the app. App generates a token1 and stores token id to particular user in DB.
  2. User2 logs into the app. App generates a token2 and stores token id to particular user in DB.
  3. User1 makes another request. App validates token including token id against token id stored in DB. If token ids are not same, forbidden response is returned.

IdentityService

IdentityService contains a new method ValidateToken that compares token ids. The result of the comparison is added as a Claim with name ValidTokenId into the TokenValidatedContext. Updated IndetityService looks as follows:

  public class IdentityService
    {
        public static string ValidTokenId = "ValidTokenId";

        private static readonly Random Random = new Random();

        private readonly Configuration configuration;

        public IdentityService(Configuration configuration)
        {
            this.configuration = configuration;
        }

        public async Task ValidateToken(TokenValidatedContext context)
        {
            var claims = new List();
            var sub = (context.SecurityToken as JwtSecurityToken)?.Claims
                .FirstOrDefault(e => e.Type == JwtRegisteredClaimNames.Sub)?.Value;
            if (sub != null)
            {
                Common.Db.Model.User user;
                using (var dbContext = new DatabaseContext(DatabaseContext.GetDbOptions(this.configuration.DbConnection)))
                {
                    user = dbContext
                        .Users
                        .AsNoTracking()
                        .FirstOrDefault(e => string.Equals(e.Email, sub, StringComparison.OrdinalIgnoreCase));
                }

                if (user != null)
                {
                    claims.Add(new Claim(ValidTokenId, user.UserTokenId == context.SecurityToken.Id ? "true" : "false", ClaimValueTypes.Boolean));
                    var claimsIdentity = context.Ticket.Principal.Identity as ClaimsIdentity;
                    claimsIdentity.AddClaims(claims);
                }
            }
            await Task.CompletedTask;
        }

        private static string GetSalt()
        {
            var bytes = new byte[128 / 8];
            using (var keyGenerator = RandomNumberGenerator.Create())
            {
                keyGenerator.GetBytes(bytes);
                return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLower();
            }
        }
private string GeneratePasswordHash(string password, string salt)
        {
            // NOTE: Here you should generate the password hash by your own
// algorithm :).
        }

        public string GetPasswordHash(string password)
        {
            return this.GeneratePasswordHash(password, GetSalt());
        }

        public bool IsPasswordValid(string password, string passwordHash)
        {
            var salt= passwordHash; // NOTE: Get salt frompasswordHash
            var hash = this.GeneratePasswordHash(password, salt);
            return passwordHash == hash;
        }

        public string GetRandomString(int length = 16)
        {
            const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            return new string(Enumerable.Repeat(Chars, length)
                .Select(s => s[Random.Next(s.Length)]).ToArray());
        }

        public JwtToken GenerateToken(JwtConfig jwtConfig, Credentials credentials, IEnumerable roles)
        {
            var now = DateTime.UtcNow;
            var tokenId = Guid.NewGuid().ToString();

            // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
            // You can add other claims here, if you want:
            var claims = new List;
            {
                new Claim(JwtRegisteredClaimNames.Sub, credentials.Email),
                new Claim(ClaimTypes.Name, credentials.Email),
                new Claim(JwtRegisteredClaimNames.Jti, tokenId),
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeToUnixSeconds(now).ToString(),
                    ClaimValueTypes.Integer64),
                new Claim(JwtRegisteredClaimNames.Iss, jwtConfig.Issuer)
            };

            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role.Name));
            }

            // Create the JWT and write it to a string
            var jwt = new JwtSecurityToken(
                jwtConfig.Issuer,
                jwtConfig.Audience,
                claims,
                now,
                now.Add(jwtConfig.Expiration),
                jwtConfig.SigningCredentials);

            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            var response = new JwtToken
            {
                AccessToken = encodedJwt,
                ExpiresIn = (int)jwtConfig.Expiration.TotalSeconds,
                TokenId = tokenId
            };

            return response;
        }

        private static long DateTimeToUnixSeconds(DateTime date)
        {
            return (long)date.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
        }
    }

Startup.cs

A JWT configuration must be updated to call IdentityService.ValidateToken. Property JwtBearerOptions.Events must be updated as follows:

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
            DatabaseContext databaseContext, Configuration configuration)
        {
            CompositionRoot.SetProvider(app.ApplicationServices);

            var jwtConfig = configuration.JwtConfig;
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SecretKey));
            jwtConfig.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
            jwtConfig.SecretKey = null;

            var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                // Validate the JWT Issuer (iss) claim
                ValidateIssuer = true,
                ValidIssuer = jwtConfig.Issuer,

                // Validate the JWT Audience (aud) claim
                ValidateAudience = true,
                ValidAudience = jwtConfig.Audience,

                // Validate the token expiry
                ValidateLifetime = true,

                // If you want to allow a certain amount of clock drift, set that here:
                ClockSkew = TimeSpan.Zero
            };

            var identityService = CompositionRoot.Resolve();

            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                Audience = jwtConfig.Audience,
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                TokenValidationParameters = tokenValidationParameters,
                Events = new JwtBearerEvents
                {
                    OnTokenValidated = identityService.ValidateToken,
                    OnChallenge = context => { return Task.CompletedTask; }
                }
            });

Authorize Attribute

To validate particular claim ValidTokenId,  authorize attribute must be extended with this parameter:

[Authorize(“ValidTokenId”)]

Conclusion

There are probably other possibilities how to ensure single user login in app depending on particular requirements. In our case, if another user logs in with the same account as another user did before, after the first user tries to request the app again, the request is revoked with HTTP error 401. This satisfies that only single user can be logged in via an account – the last one (wins).

Advertisement
dev

Authentication in .NET WebApi Core via JWT Token

Outline

After a log time, there is another post, now about JWT in .NET WebApi Code. In .NET WebApi Core there are multiple possibilities how provide authentication and they can be selected during creating a new project from template. Even there exists libraries for JWT, there is no template that generates stub with this authentication automatically.

There exist other similar solutions, mainly via using middleware that handles specific route, e.g., api/token that returns particular token:

But I think that middleware is not needed for this issue. In this case, I want to return (generate) a JWT token in two cases:

  • User login: User set credentials -> API validates credential -> If credentials  are valid, API returns Ok response with generated token. Otherwise it returns forbidden response.
  • User sign up: User posts data to create a  new user account -> API creates a new record and returns new JWT token.

How To

Prerequisites

To provide JWT support, we need to install Microsoft.AspNetCore.Authentication.JwtBearer NuGet package.

IdentitySerivice

First, we create a service for identity management. It contains all we need:

  • Methods for user password encoding
  • Methods for password validation
  • Method for JWT token generation
    public class IdentityService
    {
        private static readonly Random Random = new Random();

        private readonly Configuration configuration;

        public IdentityService(Configuration configuration)
        {
            this.configuration = configuration;
        }

        private static string GetSalt()
        {
            var bytes = new byte[128 / 8];
            using (var keyGenerator = RandomNumberGenerator.Create())
            {
                keyGenerator.GetBytes(bytes);
                return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLower();
            }
        }

        private string GeneratePasswordHash(string password, string salt)
        {
            // NOTE: Here you should generate a password hash.
            // For some security issue, I will not provide my algorithm :).
        }

        public string GetPasswordHash(string password)
        {
            return this.GeneratePasswordHash(password, GetSalt());
        }

        public bool IsPasswordValid(string password, string passwordHash)
        {
            var salt = passwordHash; // NOTE: Here you must salt from your passwordHash
            var hash = this.GeneratePasswordHash(password, salt);
            return passwordHash == hash;
        }
        public string GetRandomString(int length = 16)
        {
            const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            return new string(Enumerable.Repeat(Chars, length)
                .Select(s => s[Random.Next(s.Length)]).ToArray());
        }

        public JwtToken GenerateToken(JwtConfig jwtConfig, Credentials credentials, IEnumerable<Role> roles)
        {
            var now = DateTime.UtcNow;
            var tokenId = Guid.NewGuid().ToString();

            // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
            // You can add other claims here, if you want:
            var claims = new List<Claim>;
            {
                new Claim(JwtRegisteredClaimNames.Sub, credentials.Email),
                new Claim(ClaimTypes.Name, credentials.Email),
                new Claim(JwtRegisteredClaimNames.Jti, tokenId),
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeToUnixSeconds(now).ToString(),
                    ClaimValueTypes.Integer64),
                new Claim(JwtRegisteredClaimNames.Iss, jwtConfig.Issuer)
            };

            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role.Name));
            }

            // Create the JWT and write it to a string
            var jwt = new JwtSecurityToken(
                jwtConfig.Issuer,
                jwtConfig.Audience,
                claims,
                now,
                now.Add(jwtConfig.Expiration),
                jwtConfig.SigningCredentials);

            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            var response = new JwtToken
            {
                AccessToken = encodedJwt,
                ExpiresIn = (int)jwtConfig.Expiration.TotalSeconds,
                TokenId = tokenId
            };

            return response;
        }

        private static long DateTimeToUnixSeconds(DateTime date)
        {
            return (long)date.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
        }
    }

Where

    public class JwtConfig
    {
        public string Path { get; set; }

        public string Issuer { get; set; }

        public string Audience { get; set; }

        public string SecretKey { get; set; }

        public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(31);

        public Microsoft.IdentityModel.TokensSigningCredentials SigningCredentials { get; set; }
    }
}

and

public class Credentials
    {
        public string Email { get; set; }

        public string Password { get; set; }
    }
}

The main method GenerateToken takes as arguments JwtConfig which contains data for token generation, e.g., secret key, expiration. Next it takes additional parameters that are added into the token, e.g., user credentials (email) and roles.

User Login

To generate token during login, we need method IdentityService.IsPasswordValid. The method validates user credentials and if true, it generates and returns a new token.

        // [HttpPost] Commented because of WordPress code pretty print
        public async Task<IActionResult> PostLogin([FromBody] Login userLogin)
        {
            var user = this.userRepository.Get(e => e.Email == userLogin.Email);
            if (user == null || !this.identityService.IsPasswordValid(userLogin.Password, user.Password))
            {
                return this.Forbid();
            }

            var token = this.identityService.GenerateToken(this.configuration.JwtConfig,
                new Credentials {Email = user.Email},
                user.UserRoles.Select(e => e.Role));

            user.UserTokenId = token.TokenId;
            await this.userRepository.Update(user);

            return this.Ok(new
            {
                Token = token,
                Profile = new User
                {
                    Email = user.Email,
                    FirstName = user.FirstName,
                    LastName = user.LastName,
                    CompanyInfo = new CompanyInfo
                    {
                        Name = user.CompanyInfo?.Name
                    }
                }
            });
        }

User Sign up

Sign up method creates a new user record, next it generates and returns new token.

        // [HttpPost] Commented because of WordPress code pretty print
        public async Task<IActionResult> PostCreate([FromBody] User user)
        {
            // First validate Recaptcha
            if (!await this.ValidateGoogleRecaptha(user.RecaptchaCode))
            {
                return this.BadRequest(this.GetMessage("Invalid Recaptcha"));
            }

            var roles = new[] {this.roleRepository.Get(e => e.Name == "User")};
            var userFromDb = this.autoMapperService.Mapper.Map<Common.Db.Model.User>(user);
            userFromDb.Password = this.identityService.GetPasswordHash(userFromDb.Password);

            try
            {
                await this.userRepository.Create(userFromDb, roles);
            }
            catch (DbUpdateException dbex)
            {
                var inner = dbex.InnerException as SqlException;
                if (inner != null && inner.Message.Contains("Cannot insert duplicate key row in object 'dbo.Users"))
                {
                    return this.BadRequest(this.GetMessage("User with this email already exists"));
                }

                throw;
            }

            var token = this.identityService.GenerateToken(this.configuration.JwtConfig,
                new Credentials {Email = user.Email},
                roles);

            return this.Created(
                string.Empty,
                new
                {
                    Token = token,
                    Profile = new User
                    {
                        Email = user.Email,
                        FirstName = user.FirstName,
                        LastName = user.LastName,
                        CompanyInfo = new CompanyInfo
                        {
                            Name = user.CompanyInfo?.Name
                        }
                    }
                });
        }

Token Validation in Request

To validate token in each request, the app must be configured in Startup.cs in Configure method.

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
            DatabaseContext databaseContext, Configuration configuration)
        {
            CompositionRoot.SetProvider(app.ApplicationServices);

            var jwtConfig = configuration.JwtConfig;
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SecretKey));
            jwtConfig.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
            jwtConfig.SecretKey = null;

            var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                // Validate the JWT Issuer (iss) claim
                ValidateIssuer = true,
                ValidIssuer = jwtConfig.Issuer,

                // Validate the JWT Audience (aud) claim
                ValidateAudience = true,
                ValidAudience = jwtConfig.Audience,

                // Validate the token expiry
                ValidateLifetime = true,

                // If you want to allow a certain amount of clock drift, set that here:
                ClockSkew = TimeSpan.Zero
            };

            var identityService = CompositionRoot.Resolve<IdentityService>();

            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                Audience = jwtConfig.Audience,
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                TokenValidationParameters = tokenValidationParameters
            });
}

Next, each route that require authentication must me annotated with [Authorize] attribute as in other authentication methods in ASP.NET.

Incorporate Token Into Request

When client receives generated token in response, he must store it and adds it into every request header that requires authentication. It can differs by particular technology/framework. The important is that it must start with Bearer word.

 headers['Authorization'] = `Bearer ${accessToken}`

Conclusion

JWT authentication in .NET Core WebApi is not so complicated at all. There exists NuGet package that provides its validation and only part that must be implemented is token generation and its configuration.