Open Closed

Assistance needed for custom authentication/authorization #6752


User avatar
0
hitaspdotnet created
  • MOVED FROM: here
  • ABP Framework version: v8.1.0
  • UI Type: MVC
  • Database System: MongoDB
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes/ ABP Studio Microservice
  • Exception message and full stack trace: N/A
  • Steps to reproduce the issue: N/A

Hi dear support team. I need a custom authorization flow on my ABP application please guide me. I am very confused.

I have a web application that is hosted by Telegram Web App. This is a React application implemented in NextJS. The only thing I have access to is the token that Telegram produces. It gives me a hash string and its decryption key to validate that this request was sent by a user in Telegram. I want to auto-register every user who opened my Telegram app if they are not registered before. After confirming the token sent by Telegram, which has a userId, create a user with their Telegram ID as username and give them default roles. I also want users to access their account on the dashboard, which is the ABP admin web, without the need for a password using the Telegram Login widget, it has the same algorithm and I want to authenticate users using telegram token. What is the best flow I can use to handle this in ABP Studio MS Template (openid) please?

here is an overview of the data I can use to validate request:

using System;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Linq;


namespace Telegram
{
    /// <summary>
    /// A helper class used to verify authorization data
    /// </summary>
    public class LoginWidget : IDisposable
    {
        /// <summary>
        /// How old (in seconds) can authorization attempts be to be considered valid (compared to the auth_date field)
        /// </summary>
        public long AllowedTimeOffset = 30;

        private bool _disposed = false;
        private readonly HMACSHA256 _hmac;
        private static readonly DateTime _unixStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// Construct a new <see cref="LoginWidget"/> instance
        /// </summary>
        /// <param name="token">The bot API token used as a secret parameter when checking authorization</param>
        public LoginWidget(string token)
        {
            if (token == null) throw new ArgumentNullException(nameof(token));

            using (SHA256 sha256 = SHA256.Create())
            {
                _hmac = new HMACSHA256(sha256.ComputeHash(Encoding.ASCII.GetBytes(token)));
            }
        }

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as sorted key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(SortedDictionary<string, string> fields)
        {
            if (_disposed) throw new ObjectDisposedException(nameof(LoginWidget));
            if (fields == null) throw new ArgumentNullException(nameof(fields));
            if (fields.Count < 3) return Authorization.MissingFields;

            if (!fields.ContainsKey(Field.Id) ||
                !fields.TryGetValue(Field.AuthDate, out string authDate) ||
                !fields.TryGetValue(Field.Hash, out string hash)
            ) return Authorization.MissingFields;

            if (hash.Length != 64) return Authorization.InvalidHash;

            if (!long.TryParse(authDate, out long timestamp))
                return Authorization.InvalidAuthDateFormat;

            if (Math.Abs(DateTime.UtcNow.Subtract(_unixStart).TotalSeconds - timestamp) > AllowedTimeOffset)
                return Authorization.TooOld;

            fields.Remove(Field.Hash);
            StringBuilder dataStringBuilder = new StringBuilder(256);
            foreach (var field in fields)
            {
                if (!string.IsNullOrEmpty(field.Value))
                {
                    dataStringBuilder.Append(field.Key);
                    dataStringBuilder.Append('=');
                    dataStringBuilder.Append(field.Value);
                    dataStringBuilder.Append('\n');
                }
            }
            dataStringBuilder.Length -= 1; // Remove the last \n

            byte[] signature = _hmac.ComputeHash(Encoding.UTF8.GetBytes(dataStringBuilder.ToString()));

            // Adapted from: https://stackoverflow.com/a/14333437/6845657
            for (int i = 0; i < signature.Length; i++)
            {
                if (hash[i * 2] != 87 + (signature[i] >> 4) + ((((signature[i] >> 4) - 10) >> 31) & -39)) return Authorization.InvalidHash;
                if (hash[i * 2 + 1] != 87 + (signature[i] & 0xF) + ((((signature[i] & 0xF) - 10) >> 31) & -39)) return Authorization.InvalidHash;
            }

            return Authorization.Valid;
        }

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(Dictionary<string, string> fields)
        {
            if (fields == null) throw new ArgumentNullException(nameof(fields));
            return CheckAuthorization(new SortedDictionary<string, string>(fields, StringComparer.Ordinal));
        }

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(IEnumerable<KeyValuePair<string, string>> fields) =>
            CheckAuthorization(fields?.ToDictionary(f => f.Key, f => f.Value, StringComparer.Ordinal));

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(IEnumerable<Tuple<string, string>> fields) =>
            CheckAuthorization(fields?.ToDictionary(f => f.Item1, f => f.Item2, StringComparer.Ordinal));

        public void Dispose()
        {
            if (!_disposed)
            {
                _disposed = true;
                _hmac?.Dispose();
            }
        }

        private static class Field
        {
            public const string AuthDate = "auth_date";
            public const string Id = "id";
            public const string Hash = "hash";
        }
    }
    
    public enum Authorization
    {
        InvalidHash,
        MissingFields,
        InvalidAuthDateFormat,
        TooOld,
        Valid
    }
}

8 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Copy from https://support.abp.io/QA/Questions/6747/How-to-generate-access-token-in-backend-only-with-user-email#answer-3a10fd1e-cf4b-52f5-07f1-41607c71cdbe

    Since the topic is the same, I want to raise my question here. Please reduce my tickets and guide me. I am very confused.

    I have a web application that is hosted by Telegram Web App. This is a React application implemented in NextJS. The only thing I have access to is the token that Telegram produces. It gives me a hash string and its decryption key to validate that this request was sent by a user in Telegram. I want to auto-register every user who opened my Telegram app if they are not registered before. After confirming the token sent by Telegram, which has a userId, create a user with their Telegram ID as username and give them default roles. I also want users to access their account on the dashboard, which is the ABP admin web, without the need for a password using the Telegram Login widget, it has the same algorithm and I want to authenticate users using telegram token. What is the best flow I can use to handle this in ABP Studio MS Template (openid) please?

    here is an overview of the data I can use to validate request:

    using System;
    using System.Text;
    using System.Collections.Generic;
    using System.Security.Cryptography;
    using System.Linq;
    
    
    namespace Telegram
    {
        /// <summary>
        /// A helper class used to verify authorization data
        /// </summary>
        public class LoginWidget : IDisposable
        {
            /// <summary>
            /// How old (in seconds) can authorization attempts be to be considered valid (compared to the auth_date field)
            /// </summary>
            public long AllowedTimeOffset = 30;
    
            private bool _disposed = false;
            private readonly HMACSHA256 _hmac;
            private static readonly DateTime _unixStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    
            /// <summary>
            /// Construct a new <see cref="LoginWidget"/> instance
            /// </summary>
            /// <param name="token">The bot API token used as a secret parameter when checking authorization</param>
            public LoginWidget(string token)
            {
                if (token == null) throw new ArgumentNullException(nameof(token));
    
                using (SHA256 sha256 = SHA256.Create())
                {
                    _hmac = new HMACSHA256(sha256.ComputeHash(Encoding.ASCII.GetBytes(token)));
                }
            }
    
            /// <summary>
            /// Checks whether the authorization data received from the user is valid
            /// </summary>
            /// <param name="fields">A collection containing query string fields as sorted key-value pairs</param>
            /// <returns></returns>
            public Authorization CheckAuthorization(SortedDictionary<string, string> fields)
            {
                if (_disposed) throw new ObjectDisposedException(nameof(LoginWidget));
                if (fields == null) throw new ArgumentNullException(nameof(fields));
                if (fields.Count < 3) return Authorization.MissingFields;
    
                if (!fields.ContainsKey(Field.Id) ||
                    !fields.TryGetValue(Field.AuthDate, out string authDate) ||
                    !fields.TryGetValue(Field.Hash, out string hash)
                ) return Authorization.MissingFields;
    
                if (hash.Length != 64) return Authorization.InvalidHash;
    
                if (!long.TryParse(authDate, out long timestamp))
                    return Authorization.InvalidAuthDateFormat;
    
                if (Math.Abs(DateTime.UtcNow.Subtract(_unixStart).TotalSeconds - timestamp) > AllowedTimeOffset)
                    return Authorization.TooOld;
    
                fields.Remove(Field.Hash);
                StringBuilder dataStringBuilder = new StringBuilder(256);
                foreach (var field in fields)
                {
                    if (!string.IsNullOrEmpty(field.Value))
                    {
                        dataStringBuilder.Append(field.Key);
                        dataStringBuilder.Append('=');
                        dataStringBuilder.Append(field.Value);
                        dataStringBuilder.Append('\n');
                    }
                }
                dataStringBuilder.Length -= 1; // Remove the last \n
    
                byte[] signature = _hmac.ComputeHash(Encoding.UTF8.GetBytes(dataStringBuilder.ToString()));
    
                // Adapted from: https://stackoverflow.com/a/14333437/6845657
                for (int i = 0; i < signature.Length; i++)
                {
                    if (hash[i * 2] != 87 + (signature[i] >> 4) + ((((signature[i] >> 4) - 10) >> 31) & -39)) return Authorization.InvalidHash;
                    if (hash[i * 2 + 1] != 87 + (signature[i] & 0xF) + ((((signature[i] & 0xF) - 10) >> 31) & -39)) return Authorization.InvalidHash;
                }
    
                return Authorization.Valid;
            }
    
            /// <summary>
            /// Checks whether the authorization data received from the user is valid
            /// </summary>
            /// <param name="fields">A collection containing query string fields as key-value pairs</param>
            /// <returns></returns>
            public Authorization CheckAuthorization(Dictionary<string, string> fields)
            {
                if (fields == null) throw new ArgumentNullException(nameof(fields));
                return CheckAuthorization(new SortedDictionary<string, string>(fields, StringComparer.Ordinal));
            }
    
            /// <summary>
            /// Checks whether the authorization data received from the user is valid
            /// </summary>
            /// <param name="fields">A collection containing query string fields as key-value pairs</param>
            /// <returns></returns>
            public Authorization CheckAuthorization(IEnumerable<KeyValuePair<string, string>> fields) =>
                CheckAuthorization(fields?.ToDictionary(f => f.Key, f => f.Value, StringComparer.Ordinal));
    
            /// <summary>
            /// Checks whether the authorization data received from the user is valid
            /// </summary>
            /// <param name="fields">A collection containing query string fields as key-value pairs</param>
            /// <returns></returns>
            public Authorization CheckAuthorization(IEnumerable<Tuple<string, string>> fields) =>
                CheckAuthorization(fields?.ToDictionary(f => f.Item1, f => f.Item2, StringComparer.Ordinal));
    
            public void Dispose()
            {
                if (!_disposed)
                {
                    _disposed = true;
                    _hmac?.Dispose();
                }
            }
    
            private static class Field
            {
                public const string AuthDate = "auth_date";
                public const string Id = "id";
                public const string Hash = "hash";
            }
        }
        
        public enum Authorization
        {
            InvalidHash,
            MissingFields,
            InvalidAuthDateFormat,
            TooOld,
            Valid
        }
    }
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You can check this: https://github.com/abpframework/abp/issues/4977#issuecomment-670006297

    Add your TelegramExternalLoginProvider

  • User Avatar
    0
    hitaspdotnet created

    hi

    You can check this: https://github.com/abpframework/abp/issues/4977#issuecomment-670006297

    Add your TelegramExternalLoginProvider

    let me try to be clearer. this is a telegram web application. as you see, telegram user already detected. I used same algorithm but client side (nextjs - react). I need to authorize these users and their permissions in backend. telegram web app has force dependency to telegram bot, so we have to create a bot if we want to launch a telegram web app. so, I decided to mark bots as our tenants and app's users as tenant users. I created a custom tenant resolver to get bot ID from request. Also, I created a custom grant_type and I added it to bot client's scops to accept requests from the bot. I need an endpoint to post data: auth_data, id, hash and get token to store it in the client storage and making queries to the API using this token based on their permissions. if received data is valid but requester user is not existed in ABP user db then should create it with default role and not be rejected, then generate and send token.

    I also have a criticism; I have been a fan of ABP team since ZERO and have always followed your work because of my interest in your team. But the support forum is more like GitHub issue and only the bugs of the framework itself are well supported.

  • User Avatar
    0
    hitaspdotnet created

    So far, the framework's identity module has met all the infrastructure I needed, and I never had to customize it. I have no idea where to start. I'm not asking you for code, I just want to know where I should go to customize ABP authentication for this or any similar scenarios. I appreciate your guidance.

    Best regards.

  • User Avatar
    1
    maliming created
    Support Team Fullstack Developer

    hi

    I need an endpoint to post data: auth_data, id, hash and get token to store it in the client storage and making queries to the API using this token based on their permissions. if received data is valid but requester user is not existed in ABP user db then should create it with default role and not be rejected, then generate and send token.

    You can add a custom grant type to your project. then create a user if necessary and generate an access token.

    This new grant-type endpoint will authenticate your telegram user.

    https://community.abp.io/posts/how-to-add-a-custom-grant-type-in-openiddict.-6v0df94z

    As a web framework, it's basically request/response. You can use openiddict's custom grant type or add an API controller to handle requests and respond with data. If you wish to generate an openiddict access token, you'd better create a custom grant type

  • User Avatar
    0
    hitaspdotnet created

    Thank you @maliming. You are awesome. is it best practice to add whole process inside custom grant type?

    I have a host-side entity to store keys for hash decryption: key = $"{ICurrrentTenant.Name}:{TelegramBotApiToken}". tenant name is same as bot username. here are steps I need to do to grant a request:

    1. Get telegram data from request parameters
    2. Inject keyRepository and get current tenant's key
    3. validate hash from step 1
    4. get user's userName from telegram data
    5. check if already existed else create it and assign default roles.
    6. create principals
    7. set scops, claims and resources
    8. handle principal and log security
    9. return sign in result

    then the token and refresh token are accessible at /connect/token?

  • User Avatar
    1
    maliming created
    Support Team Fullstack Developer

    hi

    is it best practice to add whole process inside custom grant type?

    I tend to do this.

    then the token and refresh token are accessible at /connect/token?

    Yes, Because your access_token is legal.

    You can add some simple logic checks in your custom grant type. and then integrate Telegram.

  • User Avatar
    0
    hitaspdotnet created

    hi

    is it best practice to add whole process inside custom grant type?

    I tend to do this.

    then the token and refresh token are accessible at /connect/token?

    Yes, Because your access_token is legal.

    You can add some simple logic checks in your custom grant type. and then integrate Telegram.

    Thank you very much sir. You gave me a very clear idea. I decided to implement user creator in a separate service. First, I will search for the identityUser via loginInfo by the provider infos, if it is not found, I will reject the request in custom grant type. Then I check the response in the client side, if the request is rejected, I'll send a request with telegram user's info to the semi-fake user creator service.

Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11