Human Captcha (Not Robot) React Component with .Net Core WebApi Backend

A captcha is a challenge response test to determine whether or not the user is human. We may create a React human captcha component with a .Net Core Web API back-end. 

Back-end

Our back-end is ASP.Net Core Web API 3.1. Back-end will generate a truly random alpha-numeric captcha value, creates a binary image using System.Drawing namespace. Back-end will verify the user input value against the generated value and  return an authentication Token to be used in Forms.

The method that generates a random captcha value and creates a binary image is as follows:

        /// <summary>
        /// generate captcha image
        /// </summary>
        /// <param name="size">alphanumeric length of the captcha</param>
        /// <returns>image properties and image binary data</returns>
        private async Task<CaptchaImage> generateCaptcha(int size)
        {
            try
            {
                int width = size * 30;
                int height = width / 4;
                var b = new Bitmap(width, height);
                var g = Graphics.FromImage(b);
                //create brush and rectangle
                var brush = new HatchBrush(HatchStyle.SmallConfetti, Color.LightSteelBlue, Color.White);
                g.FillRectangle(brush, 0, 0, b.Width, b.Height);
                float emSize = width / size;
                var fnt = new Font("Arial", emSize, FontStyle.Italic);
                // generate truly random captcha value
                string value = generateRandomCaptchaValue(size);
                while (string.IsNullOrEmpty(value))
                    value = generateRandomCaptchaValue(size);
                // write the generated value on the image
                g.DrawString(value, fnt, Brushes.Coral, 0, 0, StringFormat.GenericTypographic);
                // generate random noise on the image
                brush = new HatchBrush(HatchStyle.LargeConfetti, Color.SlateBlue, Color.SlateGray);
                fillRandomNoise(g, brush, width, height);
                // draw random lines on the image
                Point[] iP = getRandomPoints(width, height);
                for (int i = 0; i < 3; i++)
                {
                    Brush brs = Brushes.BlueViolet;
                    if (i % 3 == 1)
                        brs = Brushes.DodgerBlue;
                    if (i % 3 == 2)
                        brs = Brushes.MediumVioletRed;
                    Pen pn = new Pen(brs, 2);
                    g.DrawBezier(pn, iP[i * 4], iP[i * 4 + 1], iP[i * 4 + 2], iP[i * 4 + 3]);
                }
                // create image
                byte[] bBuffer = (byte[])System.ComponentModel.TypeDescriptor.GetConverter(b).ConvertTo(b, typeof(byte[]));
                // add image properties to database
                var guid = await saveCaptcha(value);
                // return image with properties
                if (guid.HasValue)
                    return new CaptchaImage() { ID = guid.Value, Value = value, Data = bBuffer, MimeType = "image/jpg" };
            }
            catch(System.Exception e)
            {
                exceptionManager.DoException(e);
            }        
            return null;
        }

The method verifyCaptcha verifies the user input value against the generated value and  returns an authentication Token to the client to be used in Forms

        private async Task<ResultItem> verifyCaptcha(Guid imageid, string value)
        {
            try
            {
                var item = await context.Captchas.Where(c => c.Guid.Equals(imageid) && c.Value.Equals(value) && c.Expires >= DateTime.Now).FirstOrDefaultAsync();
                if (item != null)
                {
                    var tokenModel = new TokenModel(this.context, this.exceptionManager);
                    string token = await tokenModel.SaveToken(imageid);
                    if (!string.IsNullOrEmpty(token))
                        return new ResultItem() { Result = true, Token = token };
                }
            }
            catch(System.Exception e)
            {
                exceptionManager.DoException(e);
            }
            return new ResultItem() { Result = false, Token = string.Empty };
        }

Front-end

Our React component will request from the back-end to generate a random captcha value and the captcha image that the generated value is drawn on it. React component includes the captcha image and a text field and a button to verify the user is not a robot. 

    async getCaptcha(size) {
        var id = "";
        var url = "";
        try {
            const response = await fetch(API + QUERY + "/" + size);
            const data = await response.json();
            id = data.id;
            const blob = this.convertToImage(data.data, data.mimeType);
            url = URL.createObjectURL(blob);
        }
        catch (error) {
            console.log(error);
        }
        this.setState({ id: id, url: url, size: size });
    }

Verify button will request from back-end if the user input value is matched with the generated value. If the captcha is verified, human captcha React component will pass an authentication Token provided from back-end to its parent component.

    async validateCaptcha(id, value, callback) {
        let url = API + QUERY;
        let data = { id: id, value: value };
        var result = false;
        var token = "";
        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data)
            });
            const resultData = await response.json();
            if (resultData) {
                result = resultData.result;
                token = resultData.token;
            }
        }
        catch (error) {
            console.log(error);
        }
        this.setState({ token: token, result: result });
        callback();
    }

    handleValidate(event) {
        event.preventDefault();
        const id = this.state.id;
        const value = this.state.value;

        // update parent props
        this.validateCaptcha(id, value, () => {
            if (this.props.onChange && this.state.token) {
                this.props.onChange(this.state.token);
            }
        });
    }

Parent component that uses human captcha component will use the authentication token provided from its child human captcha component to submit the Form. 

    // human captcha component
    <HumanCaptcha size="6" onChange={this.handleChangeCaptcha} />

    // authentication token passed from human captcha child component
    handleChangeCaptcha(token) {
        if (token)
            this.setState({ token: token, notbot: true });
    }

Token must be provided in the header of the post request when submitting the form:

        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Token': token
                },
                body: JSON.stringify(data)
            });
            const resultData = await response.json();
            if (resultData) {
                result = resultData.result;
                message = resultData.message;
            }
        }
        catch (error) {
            console.log(error);
        }

The post method that handles the form submit in the back-end controller will use the authentication token before processing. This could be achieved by generating a class TokenAuthenticationAttribute that inherits ActionFilterAttribute and decorating the method with TokenAuthentication (*).

        [TypeFilter(typeof(TokenAuthenticationAttribute))]
        [HttpPost]
        public SubmitResultItem Post(string id)
        {
            return new SubmitResultItem() { Result = true, Message = "Record saved." };
        }

TokenAuthentication action filter should be defined in ConfigureServices of startup for dependency injection:

        services.AddScoped<TokenAuthenticationAttribute>();

All source codes are included in a GitHub repository. Human Captcha github repository contains a .net core web-api back-end project and React front-end project.

(*) For details take a look at the blog post about Filter Based Authorization

Comments

Popular posts from this blog

Custom ActionResult for Files in ASP.NET MVC - ExcelResult

Platform independent simple password manager application (Electron & NodeJS)

Filtering html select listbox items